<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Blog | Tristan Kernan</title><link href="https://blog.tmk.name/" rel="alternate"></link><link href="https://blog.tmk.name/feeds/all.atom.xml" rel="self"></link><id>https://blog.tmk.name/</id><updated>2026-04-13T00:00:00-04:00</updated><subtitle>
“That some of us should venture to embark on a synthesis of facts and theories, albeit with second-hand and incomplete knowledge of some of them – and at the risk of making fools of ourselves” (Erwin Schrödinger)
</subtitle><entry><title>Good List Week 3</title><link href="https://blog.tmk.name/2026/04/13/good-list-week-3/" rel="alternate"></link><published>2026-04-13T00:00:00-04:00</published><updated>2026-04-13T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-04-13:/2026/04/13/good-list-week-3/</id><summary type="html">&lt;p&gt;Another week, another list.&lt;/p&gt;
&lt;h2&gt;Public libraries&lt;/h2&gt;
&lt;p&gt;I've been utilizing public libraries a lot the past few months, especially for books: the interlibrary loan system is a life hack, the network that includes my local library meets about 90% of my requests. The experience couldn't be easier: search for the book …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Another week, another list.&lt;/p&gt;
&lt;h2&gt;Public libraries&lt;/h2&gt;
&lt;p&gt;I've been utilizing public libraries a lot the past few months, especially for books: the interlibrary loan system is a life hack, the network that includes my local library meets about 90% of my requests. The experience couldn't be easier: search for the book in the library catalog, request it to my local branch, and pick it up in a few days.&lt;/p&gt;
&lt;p&gt;Last week I met up with a friend to work at the Maplewood public library. It's recently renovated, and situated right on a beautiful park with a turtle pond.&lt;/p&gt;
&lt;h2&gt;Grounds for Sculpture&lt;/h2&gt;
&lt;p&gt;&lt;img alt="monet" src="https://blog.tmk.name/images/gfs/monet.jpg" /&gt;
&lt;img alt="painter" src="https://blog.tmk.name/images/gfs/painter.jpg" /&gt;
&lt;img alt="peacock" src="https://blog.tmk.name/images/gfs/peacock.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I visited the Grounds for Sculpture in Hamilton - what a delight! I had recently learned the word &lt;em&gt;kitsch&lt;/em&gt;, loosely meaning tacky imitative art, and just in time, because oh boy are some of these artworks kitsch! All the same, the grounds and gardens are beautiful to walk around, and the cherry blossoms were in bloom, making for a perfect spring day.&lt;/p&gt;
&lt;h2&gt;Saravanaa Bhavan&lt;/h2&gt;
&lt;p&gt;Dark horse pick here, but I was happy to find and introduce someone to my favorite global chain restaurant. I may be one of their most well traveled customers, having visited locations in the US, UK, Europe, India and Asia. A well crafted dosa just hits the spot sometimes. For decent Indian vegetarian food almost anywhere in the world, it can't be beat.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Good List Week 2</title><link href="https://blog.tmk.name/2026/04/06/good-list-week-2/" rel="alternate"></link><published>2026-04-06T00:00:00-04:00</published><updated>2026-04-06T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-04-06:/2026/04/06/good-list-week-2/</id><summary type="html">&lt;p&gt;Continuing on the gratitude practice of recognizing the good in the world - here's my good list for the past week.&lt;/p&gt;
&lt;h2&gt;Blossoms&lt;/h2&gt;
&lt;p&gt;It's spring in New Jersey, and that means the flowers are starting to bloom. Essex county is home to one of the largest cherry blossom populations outside of Japan …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Continuing on the gratitude practice of recognizing the good in the world - here's my good list for the past week.&lt;/p&gt;
&lt;h2&gt;Blossoms&lt;/h2&gt;
&lt;p&gt;It's spring in New Jersey, and that means the flowers are starting to bloom. Essex county is home to one of the largest cherry blossom populations outside of Japan, if I remember the Branch Brook Park signs correctly. While that park gets absolutely mobbed during cherry blossom season, you don't have to work hard to see the blossoms: they are just about everywhere in the area. Here's a lone cherry blossom tree I spotted at Hilltop Reservation:&lt;/p&gt;
&lt;p&gt;&lt;img alt="cherry blossom" src="https://blog.tmk.name/images/cherry-blossom.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;After that brutal winter, it's nice to feel the warmth of springtime.&lt;/p&gt;
&lt;h2&gt;Volunteering&lt;/h2&gt;
&lt;p&gt;I volunteered at two drug and alcohol rehabs last week, speaking to over a hundred people about family and generational trauma. The response is always moving, with people regularly opening up about experiences that they've never told anyone before. I was reminded of the power of the work when a tough-looking guy stopped speaking mid-sentence and left the room to cry. The level of identification is humbling: it shows that beyond race, class, background, so many of us shared similar experiences. It's said that service to others is a pillar of a good life, and I'm grateful for the opportunity to be of service.&lt;/p&gt;
&lt;figure&gt;
&lt;blockquote&gt;
&lt;p&gt;He that plants trees loves others besides himself&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figcaption&gt;
&lt;p&gt;Thomas Fuller&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;House plants&lt;/h2&gt;
&lt;p&gt;I picked up a few new house plants last week at a local nursery. I added money tree and tradescantia to my collection, which also includes: snake plant, monsteria, zz plant, pothos, and pepperomia. Having life in my apartment, and creatures to care for, adds light to my life.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Good List Week 1</title><link href="https://blog.tmk.name/2026/03/30/good-list-week-1/" rel="alternate"></link><published>2026-03-30T00:00:00-04:00</published><updated>2026-03-30T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-03-30:/2026/03/30/good-list-week-1/</id><summary type="html">&lt;p&gt;I heard on the radio today that the New York Times started publishing a "good list" of fun activities as experienced by reporters. The idea being to share the positive, on-the-ground experiences; get off-line and remember that life is to be lived, not watched from the sideline (or screen). With …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I heard on the radio today that the New York Times started publishing a "good list" of fun activities as experienced by reporters. The idea being to share the positive, on-the-ground experiences; get off-line and remember that life is to be lived, not watched from the sideline (or screen). With spring in the air, I feel inspired to do the same, starting with a backlog of the past month or so:&lt;/p&gt;
&lt;h2&gt;High school musical&lt;/h2&gt;
&lt;p&gt;Last year, I discovered Morristown high school's theatrer department when they put on Les Mis. It was amazing! The talent from the students really impressed me, from the performance, to the stage and costumes. I've since been back for the fall presentation of Little Women, and their recent spring musical Fiddler on the Roof.&lt;/p&gt;
&lt;p&gt;Great way to spend $15 for a quality performance in the local community.&lt;/p&gt;
&lt;h2&gt;Opera&lt;/h2&gt;
&lt;p&gt;I attended the opera at the Met for the first time, just last Friday. I had some fear that, with the language barrier, and Timothée Chalamet's recent comments, I'd not enjoy the show - but I was pleasantly surprised. Language-wise, each seat has its own subtitles, making following the story easy. I put on my khakis, expecting folks to be dressed up - I might as well have worn blue jeans, compared to the ballroom gowns and dapper suits I was surrounded with. The performance itself, Madame Butterfly, was exquisite - I was mesmerized by the stage design, the use of simple lights and mirrors to incredible effect:&lt;/p&gt;
&lt;p&gt;&lt;img alt="madame butterfly" src="https://blog.tmk.name/images/madamebutterfly.jpg" /&gt;&lt;/p&gt;
&lt;h2&gt;Birding&lt;/h2&gt;
&lt;p&gt;Birding was recommended to me as a way to be more present and engaged. And it certainly has that effect. Typically when taking a walk or hike, I'm unconsciously trying to set my personal best. With birding, I am significantly slower, walking slower and pausing occasionally so as to listen and look. I'm encouraged to explore new parks in the region, to see what different species are present. To that end my recent hotspots are Eagle Rock Reservation, Verona Park, and Hilltop Reservation.&lt;/p&gt;
&lt;p&gt;&lt;img alt="birding trip report" src="https://blog.tmk.name/images/birdingathilltop.png" /&gt;&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Three Years of Fellowship</title><link href="https://blog.tmk.name/2026/03/27/three-years-of-fellowship/" rel="alternate"></link><published>2026-03-27T00:00:00-04:00</published><updated>2026-03-27T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-03-27:/2026/03/27/three-years-of-fellowship/</id><summary type="html">&lt;p&gt;This is a follow-up to my &lt;a href="https://blog.tmk.name/2025/03/24/two-years-of-fellowship/"&gt;two year reflection post last year&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Driving a car off a cliff&lt;/h2&gt;
&lt;p&gt;A metaphor I've often used for my life is that of driving a car off a cliff. Growing up in chaos and dysfunction, I lived my adult life in chaos and dysfunction …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is a follow-up to my &lt;a href="https://blog.tmk.name/2025/03/24/two-years-of-fellowship/"&gt;two year reflection post last year&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Driving a car off a cliff&lt;/h2&gt;
&lt;p&gt;A metaphor I've often used for my life is that of driving a car off a cliff. Growing up in chaos and dysfunction, I lived my adult life in chaos and dysfunction. I had been headed towards a certain path, the particular details of which impossible to predict, but the result the same: catastrophe.&lt;/p&gt;
&lt;p&gt;I view my recovery and healing as turning the car wheel and applying the brakes. Given the momentum, these are not instant fixes; I often ask myself if I started braking early enough, or if I'm doomed to vault over the cliff. Perhaps given the past year, I can say that I stopped the car as it's teetering off the edge of the cliff, with little room to spare.&lt;/p&gt;
&lt;p&gt;These are just stories I tell myself. As I aptly heard someone share this week, there's a difference between honesty and truth. Honesty is what I believe to be true, as opposed to the objective truth. I am quite skilled at deceiving myself.&lt;/p&gt;
&lt;h2&gt;What's been going on&lt;/h2&gt;
&lt;p&gt;I am coming out of one of the most challenging years of my life. And I'm no stranger to challenging years. I'm not one for omens or precognition, but events were seemingly choreographed: I distinctly recall when multiple people shared about their physical debilitation, shortly before my own. It was a summer flu which struck me down: I had been red-lining my stress, and paid the price. It's been nigh on nine months of burnout and recovery.&lt;/p&gt;
&lt;p&gt;Work went from stable to ongoing crisis as I was forced into withdrawal from workaholism (not much of a surprise there, it runs in the family). In re-evaluating my relationship to my job, and setting healthier boundaries for myself, I discovered my boss' covert narcissism. Discover sounds like a detached scientific process; this was trial by fire, incurring my boss' narcissist wrath.&lt;/p&gt;
&lt;p&gt;It's incredible how long I can survive in chaos. There's a part of me that, like the protagonists of my beloved Culture series, believes I'm invincible. What a painful but healing revelation that I am not. It took my health deteriorating, and a coup de grâce from my boss, for that warrior part of me to step aside, which enabled me to give myself the gift of letting go.&lt;/p&gt;
&lt;p&gt;In the past few months, I've stepped back from work to focus on my health. It's been a miserable period, contrary to the "with time off, enjoy yourself!" mentality. The cognitive incapacitation from burnout has been painful in many ways: one, as an intellectual, losing my core strength; two, the uncertainty, not knowing when or if I'll recover. I am feeling better, week after week, faster than I fear, slower than I hope.&lt;/p&gt;
&lt;h2&gt;What's new&lt;/h2&gt;
&lt;p&gt;Somehow in the midst of all that, I managed to take some significant positive steps. I started volunteering, bringing a message of hope and healing to drug and alcohol rehab centers in my area. This experience has been incredible for me, coming from a family of alcoholics and addicts. Growing up, when passing the local rehab, my father would say to me, "I went there, your mother went there, and one day you'll go there." To fulfill this prophecy upside down feels like taking control of my own story.&lt;/p&gt;
&lt;p&gt;One of my goals for the last year, physical fitness, I dove into with yoga, making a consistent habit of it. After nine months of practice, my body, and my relationship to my body, feels great: decreased tension, decreased aches and pains. It's said that emotions are stored in the body, with yoga as a great means to move the emotions: this has been my experience.&lt;/p&gt;
&lt;p&gt;Another goal, travel; I visited England, Scotland, Ireland and Greece. Each trip I take changes me, or unlocks accumulated changes within me; either way, coming back I feel refreshed, invigorated, excited for life. In Greece, I solo traveled abroad for the first time, walking along the ancient marble of the Acropolis, where my ancient heroes once walked. Standing amidst history, I realized (for the nth time) the obviousness of the truth: what I'm looking for so desperately is within, not without. Spiritual insight aside, I can't wait to travel again soon.&lt;/p&gt;
&lt;h2&gt;What's next&lt;/h2&gt;
&lt;p&gt;One of the Buddhist insights I relate to strongest is that peace is available within myself, anytime. It reminds me of the recovery saying, that serenity is not freedom from the storm, but safe harbor within the storm. As the world continues to dive deeper into chaos, I am grateful for my serenity. It is my foundation stone as I once again dust myself off and venture back into the fray.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Study Guide</title><link href="https://blog.tmk.name/2026/03/23/study-guide/" rel="alternate"></link><published>2026-03-23T00:00:00-04:00</published><updated>2026-03-25T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-03-23:/2026/03/23/study-guide/</id><summary type="html">&lt;p&gt;Recently, I've started to rouse from my hibernation. As I tend to be slightly eclectic and eccentric, I want to organize (or at least document) my approach to preparing to re-join the workforce as a software engineer.&lt;/p&gt;
&lt;p&gt;I'll aim to update this page over time. &lt;/p&gt;
&lt;h2&gt;Interview practice&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Leetcode/Neetcode, leveraging …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;Recently, I've started to rouse from my hibernation. As I tend to be slightly eclectic and eccentric, I want to organize (or at least document) my approach to preparing to re-join the workforce as a software engineer.&lt;/p&gt;
&lt;p&gt;I'll aim to update this page over time. &lt;/p&gt;
&lt;h2&gt;Interview practice&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Leetcode/Neetcode, leveraging llms for interactive problem solving and hints.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Books&lt;/h2&gt;
&lt;p&gt;Done:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Machine Learning (MIT Essential Knowledge): finished; relatively recent, solid high level intro to ML for someone without the background.&lt;/li&gt;
&lt;li&gt;7 Concurrency Models in 7 Weeks: finished; interesting read, my first intro to CSP.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In-progress:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;System Design Interview: halfway through.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Queued:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;7 Databases in 7 Weeks&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aosabook.org/en/"&gt;Architecture of Open Source Applications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Projects&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.simplerouting.io/"&gt;Simple Routing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tristanmkernan/rlui"&gt;Rate Limiter UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sps.tmk.name/"&gt;Simple Paste Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sss.tmk.name/"&gt;Simple Stock Service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before you ask, yes, I do like to name things "simple xyz" - as a reaction to the ballooning complexity of most software products.&lt;/p&gt;
&lt;h2&gt;Etc&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Studying outside the home: coming from years of remote work, I am re-familiarizing myself with in-person work (cheers to Panera sip club!)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://neetcode.io"&gt;neetcode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://leetcode.com"&gt;leetcode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://systemdesignschool.io/"&gt;system design school&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="tidbits"></category></entry><entry><title>Wat</title><link href="https://blog.tmk.name/2026/03/09/wat/" rel="alternate"></link><published>2026-03-09T00:00:00-04:00</published><updated>2026-03-09T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-03-09:/2026/03/09/wat/</id><summary type="html">&lt;p&gt;I use emacs, and within emacs I use the pytest plugin to run tests. When I need to debug tests, I turn to ipdb with a quick yasnippet, dropping in &lt;code&gt;import ipdb; ipdb.set_trace()&lt;/code&gt;. When debugging with ipdb, I just love this little tool called &lt;a href="https://github.com/igrek51/wat"&gt;wat&lt;/a&gt;. It makes inspecting objects …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I use emacs, and within emacs I use the pytest plugin to run tests. When I need to debug tests, I turn to ipdb with a quick yasnippet, dropping in &lt;code&gt;import ipdb; ipdb.set_trace()&lt;/code&gt;. When debugging with ipdb, I just love this little tool called &lt;a href="https://github.com/igrek51/wat"&gt;wat&lt;/a&gt;. It makes inspecting objects a breeze, and is a must-have in all my projects now.&lt;/p&gt;
&lt;p&gt;As an example, I was debugging the difference between Django's &lt;code&gt;Request&lt;/code&gt; class and Django Rest Framework's &lt;code&gt;Request&lt;/code&gt; class, and their interop. Dropping into a debugger in the view, I could quickly inspect the request object:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;ipdb&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;wat&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;/&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;request&lt;/span&gt;
&lt;span style="color: #f88f7f"&gt;─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;value:&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;rest_framework&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;request&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;Request:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;GET&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;#39;/osrm/route/v1/driving/-74.172400,40.735700%3B-74.027600,40.744000&amp;#39;&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;&amp;gt;&lt;/span&gt;
&lt;span style="color: #FFD173"&gt;type&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;rest_framework&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;request&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;Request&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;Public&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;attributes:&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;FILES:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;utils&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;datastructures&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;MultiValueDict&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{}&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;POST:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;http&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;request&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;QueryDict&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{}&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;accepted_media_type:&lt;/span&gt; &lt;span style="color: #FFD173"&gt;str&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;#39;application/json&amp;#39;&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;data:&lt;/span&gt; &lt;span style="color: #FFD173"&gt;dict&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{}&lt;/span&gt;
  &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;reduced&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;brevity&lt;/span&gt;

  &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;force_plaintext_errors&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(value)&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;Private&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;attributes:&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;_auth:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;NoneType&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;None&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;_data:&lt;/span&gt; &lt;span style="color: #FFD173"&gt;dict&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{}&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;_files:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;utils&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;datastructures&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;MultiValueDict&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{}&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;_request:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;core&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;handlers&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;wsgi&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;WSGIRequest&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;WSGIRequest:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;GET&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;#39;/osrm/route/v1/driving/-74.172400,40.735700%3B-74.027600,40.744000&amp;#39;&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;&amp;gt;&lt;/span&gt;

  &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;_authenticate&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;()&lt;/span&gt; &lt;span style="color: #7e8aa1"&gt;# Attempt to authenticate the request using each authentication instance…&lt;/span&gt;
  &lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;_content_type&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;()&lt;/span&gt; &lt;span style="color: #7e8aa1"&gt;# Placeholder for unset attributes.…&lt;/span&gt;
  &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;reduced&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;brevity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Public and private attributes and methods are listed. Where available, types, classes, values and docstrings are displayed inline.&lt;/p&gt;
&lt;h2&gt;Auto import&lt;/h2&gt;
&lt;p&gt;It's tedious to have to import wat in every shell session, so I looked online and found that it can be automatically imported. There's two different configurations to apply.&lt;/p&gt;
&lt;p&gt;For automatic import into ipython shells (i.e. &lt;code&gt;manage.py shell&lt;/code&gt;), modify the ipython startup profile&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;, by creating a new file at &lt;code&gt;~/.ipython/profile_default/startup/00-first.py&lt;/code&gt;, with the contents &lt;code&gt;from wat import wat&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For automatic import into debugger ipdb shells (i.e. &lt;code&gt;ipdb.set_trace()&lt;/code&gt;), add &lt;code&gt;from wat import wat&lt;/code&gt; to &lt;code&gt;.pdbrc&lt;/code&gt; in your main repository folder.&lt;/p&gt;
&lt;p&gt;And - don't forget to pip install the &lt;code&gt;wat&lt;/code&gt; package in your repository 🙂&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Thanks to https://switowski.com/blog/ipython-startup-files/&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="tidbits"></category></entry><entry><title>Django: dj-stripe</title><link href="https://blog.tmk.name/2026/03/04/django-dj-stripe/" rel="alternate"></link><published>2026-03-04T00:00:00-05:00</published><updated>2026-03-04T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-03-04:/2026/03/04/django-dj-stripe/</id><summary type="html">&lt;p&gt;I spent the last week integrating Stripe into a side project. Given the popularity of the library, I expected dj-stripe to simplify the implementation and save me time. Internally, I bucketed a few hours to a complete implementation. Boy, was I wrong.&lt;/p&gt;
&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;For context, my side project is a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I spent the last week integrating Stripe into a side project. Given the popularity of the library, I expected dj-stripe to simplify the implementation and save me time. Internally, I bucketed a few hours to a complete implementation. Boy, was I wrong.&lt;/p&gt;
&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;For context, my side project is a metered api gateway. I offer a free tier and a monthly subscription tier. My basic use cases are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User signs up for subscription (via stripe checkout)&lt;/li&gt;
&lt;li&gt;User manages (cancel, edit payment) their subscription (via stripe portal)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a user signs up for a subscription, they'll be upgraded to the higher tier internally. Likewise on cancellation, they'll be downgraded.&lt;/p&gt;
&lt;h2&gt;What does dj-stripe do, anyway?&lt;/h2&gt;
&lt;p&gt;I expected dj-stripe to integrate cleanly with a Django application, through webhook handling and customer object relations. While the library does provide these functions, what I didn't realize is that dj-stripe does so much more: it actively syncs your stripe account with your database. Every product, price, transaction, subscription, tax zone, etc., etc. is sync'd into your database. This is purportedly for efficiency: rather than fetching data from the stripe api, it can be pulled in via a db query. I recognize that this is a real performance issue, at a previous company some endpoints serially queried the stripe api multiple times, leading to multi-second response times on critical paths.&lt;/p&gt;
&lt;p&gt;For a side project of my scale, though, in hindsight it was definitely overkill. I have one product, and one price; customers can be created via stripe api; subscription status can be managed through webhooks.&lt;/p&gt;
&lt;h2&gt;Documentation&lt;/h2&gt;
&lt;p&gt;The dj-stripe documentation is, bluntly, awful. It somehow fails to describe how to do much of anything that I was interested in doing; I imagine that SaaS subscription billing is common among Django developers! Where the documentation does exist, it's misleading if not outright wrong. Even popular blog posts, like &lt;a href="https://www.saaspegasus.com/guides/django-stripe-integrate/"&gt;How to Create a Subscription SaaS Application with Django and Stripe&lt;/a&gt; are confusing and lack details.&lt;/p&gt;
&lt;p&gt;As an example, let's look at the &lt;a href="https://github.com/dj-stripe/dj-stripe/blob/main/tests/apps/example/views.py#L25-L139"&gt;demo code for stripe checkout&lt;/a&gt;. It's more than a hundred lines, and manually associates Customer and User objects. Compare to my own implementation (courtesy of claude):&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;SubscribeCheckoutRedirectView&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(LoginRequiredMixin,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;View):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;post&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;request,&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;**&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;kwargs):&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;customer,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;_&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;Customer&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;get_or_create(subscriber&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;request&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;user)&lt;/span&gt;

        &lt;span style="color: #d4d2c8"&gt;success_url&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;
            &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;request&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;build_absolute_uri(reverse(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;dashboard&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;))&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;+&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;?subscribed=1&amp;quot;&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;cancel_url&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;request&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;build_absolute_uri(reverse(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;dashboard&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;))&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;+&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;?bailed=1&amp;quot;&lt;/span&gt;

        &lt;span style="color: #d4d2c8"&gt;session&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;stripe&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;checkout&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;Session&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;create(&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;customer&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;customer&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;id,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;success_url&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;success_url,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;cancel_url&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;cancel_url,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;mode&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;subscription&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;line_items&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;[&lt;/span&gt;
                &lt;span style="color: #d4d2c8"&gt;{&lt;/span&gt;
                    &lt;span style="color: #D5FF80"&gt;&amp;quot;price&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;settings&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;STRIPE_HOBBY_PRICE_ID,&lt;/span&gt;
                    &lt;span style="color: #D5FF80"&gt;&amp;quot;quantity&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #DFBFFF"&gt;1&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
                &lt;span style="color: #d4d2c8"&gt;}&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;],&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

        &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;redirect(session&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;url)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A fraction of the code - and the only usage of dj-stripe is in &lt;code&gt;Customer.get_or_create()&lt;/code&gt;, the resulting customer id automatically associating the session/transactions with the internal user.&lt;/p&gt;
&lt;h2&gt;Configuration&lt;/h2&gt;
&lt;p&gt;Which leads me to api key management in dj-stripe. This singlehandedly cost me at least an hour of time - unheard of for configuring an api key! The project is apparently migrating towards supporting multiple stripe accounts in a single application (despite nobody asking for it, as far as I can tell). So the documentation says don't put your stripe api keys in django settings, put the keys in the database - but guess what, some parts of dj-stripe still do expect the api key in django settings! So the api key has to be configured there, too. Under what name? Well, there's &lt;code&gt;STRIPE_SECRET_KEY&lt;/code&gt;, &lt;code&gt;STRIPE_LIVE_SECRET_KEY&lt;/code&gt;, &lt;code&gt;STRIPE_TEST_SECRET_KEY&lt;/code&gt;... seriously, why not have one page to describe all configuration settings, with deprecations and recommendations clearly marked?&lt;/p&gt;
&lt;p&gt;And why on earth am I storing my stripe api keys in my database!? That feels gross from a security perspective.&lt;/p&gt;
&lt;h2&gt;Webhooks&lt;/h2&gt;
&lt;p&gt;Finally, after all of the above, I'm able to actually sit down and build the key piece of functionality: webhook integration. Ah, this has also been rewritten, much to the community's chagrin; it's now exposed via django signals. Thankfully this was relatively simple to build, though the docs for most tutorials and guides are outdated since the rewrite.&lt;/p&gt;
&lt;p&gt;Dj-stripe actually manages the webhook for you, creating it via django admin. The use of random urls for the webhook confused me; is that all the security provided? I looked into the code and the database and found that dj-stripe does verify the webhook signature, with the webhook secret stored in the database. Why even bother with the random urls then? Tsk, tsk.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;My take is that dj-stripe is trying to do too much, with too little resources. The lack of documentation, the botched api key configuration migration, the multi-account use case.. I think there's a market for a significantly streamlined django + stripe integration for SaaS products, more opinionated in the sense of keeping the feature set small (with an escape hatch when outgrown), and less opinionated in the sense of standard practices like api key management.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>BirdNet-Pi</title><link href="https://blog.tmk.name/2026/02/27/birdnet-pi/" rel="alternate"></link><published>2026-02-27T00:00:00-05:00</published><updated>2026-03-25T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-27:/2026/02/27/birdnet-pi/</id><summary type="html">&lt;p&gt;After upgrading my homelab from a raspberry pi 4B to a beelink SER5 MAX, my rpi collected dust in the corner.. until I discovered &lt;a href="https://github.com/Nachtzuster/BirdNET-Pi"&gt;BirdNet Pi&lt;/a&gt;! Now, my rpi is working hard to catalog passing avian visitors:&lt;/p&gt;
&lt;p&gt;&lt;img alt="birdnetpi screenshot" src="https://blog.tmk.name/images/birdnetpi/dashboard.png" /&gt;&lt;/p&gt;
&lt;p&gt;As the community is in flux, I wanted to share my setup. &lt;/p&gt;
&lt;h2&gt;Audio …&lt;/h2&gt;</summary><content type="html">&lt;p&gt;After upgrading my homelab from a raspberry pi 4B to a beelink SER5 MAX, my rpi collected dust in the corner.. until I discovered &lt;a href="https://github.com/Nachtzuster/BirdNET-Pi"&gt;BirdNet Pi&lt;/a&gt;! Now, my rpi is working hard to catalog passing avian visitors:&lt;/p&gt;
&lt;p&gt;&lt;img alt="birdnetpi screenshot" src="https://blog.tmk.name/images/birdnetpi/dashboard.png" /&gt;&lt;/p&gt;
&lt;p&gt;As the community is in flux, I wanted to share my setup. &lt;/p&gt;
&lt;h2&gt;Audio&lt;/h2&gt;
&lt;p&gt;I found several long discussions about microphones and acoustics; as a newbie I chose a sub-$20 USB microphone on amazon: &lt;a href="https://www.amazon.com/dp/B0872CFTGK?th=1"&gt;CMTECK USB Computer Microphone G009&lt;/a&gt;. It's only been a day, but I'm already getting solid results with multiple 80-90% confidence ratings. The key points for me were the price, and the compatibility: USB-A means no dongle or adapter necessary.&lt;/p&gt;
&lt;p&gt;For more advanced suggestions, check out this &lt;a href="https://github.com/mcguirepr89/BirdNET-Pi/discussions/39"&gt;discussion on github&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://github.com/mcguirepr89/BirdNET-Pi"&gt;original BirdNet-Pi repo&lt;/a&gt; has been archived; there is an &lt;a href="https://github.com/Nachtzuster/BirdNET-Pi"&gt;active fork&lt;/a&gt;, which I decided to use. The &lt;a href="https://github.com/mcguirepr89/BirdNET-Pi/wiki/Installation-Guide"&gt;setup guide&lt;/a&gt; is straightforward; plus, the new raspberry pi imager software supports configuring the wifi connection, meaning the rpi can connect via wifi on first boot. The longest part of the process was the BirdNet-Pi installer script, which seemed to install hundreds of packages.. hair-raising, for sure, but it installed without issue.&lt;/p&gt;
&lt;p&gt;The software ecosystem is growing: there's also &lt;a href="https://github.com/tphakala/birdnet-go"&gt;Birdnet-Go&lt;/a&gt;, a complete re-write that's under active development. Under the hood, it's all powered by the Cornell Lab of Ornithology, via their trained models: see the &lt;a href="https://github.com/birdnet-team"&gt;birdnet team github&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;That's it&lt;/h2&gt;
&lt;p&gt;Everything worked out of the box! Loading up BirdNet in my browser streamed my microphone audio; after an hour or so the first bird ids started coming in. Next up, notifications when visitors stop by :)&lt;/p&gt;
&lt;h2&gt;One-month update&lt;/h2&gt;
&lt;p&gt;Ah, but my rpi ran into some issues and I took birdnet down for the time being. Here's a screenshot of the data collected over the month:&lt;/p&gt;
&lt;p&gt;&lt;img alt="birdnetpi screenshot" src="https://blog.tmk.name/images/birdnetpi/results.png" /&gt;&lt;/p&gt;</content><category term="tidbits"></category></entry><entry><title>From Cloudflare to Umami Analytics</title><link href="https://blog.tmk.name/2026/02/22/from-cloudflare-to-umami-analytics/" rel="alternate"></link><published>2026-02-22T00:00:00-05:00</published><updated>2026-02-22T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-22:/2026/02/22/from-cloudflare-to-umami-analytics/</id><summary type="html">&lt;p&gt;For a recent project, I integrated Cloudflare web analytics, as I had been using it for this blog for some time. Out of curiosity, I looked into alternative, self hosted analytics options: plausible and umami. Imagine my surprise when I see in the plausible docs that &lt;a href="https://plausible.io/vs-cloudflare-web-analytics#cloudflare-dashboard-is-inaccurate"&gt;cloudflare data is inaccurate …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;For a recent project, I integrated Cloudflare web analytics, as I had been using it for this blog for some time. Out of curiosity, I looked into alternative, self hosted analytics options: plausible and umami. Imagine my surprise when I see in the plausible docs that &lt;a href="https://plausible.io/vs-cloudflare-web-analytics#cloudflare-dashboard-is-inaccurate"&gt;cloudflare data is inaccurate&lt;/a&gt;, as stats are based on a 10% sample. I had been suspicious that something like this was occurring, owing to the round number bias: the phenomena whereby round numbers seem like estimates rather than exact calculations.&lt;/p&gt;
&lt;p&gt;As an example, for this blog, as of writing, Cloudflare reports 340 total visits in the past 30 days. Visits by country breaks down to: 90 USA, 70 Singapore, 20 Mexico, and so on. Browser breakdown is: 270 Chrome, 20 Firefox, 20 Safari, and so on. Every number in the dashboard is rounded to the nearest 10. Take a look:&lt;/p&gt;
&lt;p&gt;&lt;img alt="cloudflare web analytics dashboard" src="https://blog.tmk.name/images/analytics/cf.png" /&gt;&lt;/p&gt;
&lt;p&gt;In the Cloudflare dashboard, there's a tooltip that states:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Based on a 10% sample of page load events.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;... confirming the suspicion. Given the relatively low traffic to my blog, and even more so my new project, I wanted to do better. At the scale of a few dozen requests per day, I should be able to get complete information. Hence I turned to self hosting &lt;a href="https://umami.is/"&gt;umami&lt;/a&gt;. I chose umami over plausible after reading on reddit that self hosted plausible is limited, and folks reported positive experiences with umami. So far, so good: umami provides everything I am looking for in an analytics tool, plus it's simple to host and easy to use. Here's how it looks, since switching a few days ago:&lt;/p&gt;
&lt;p&gt;&lt;img alt="umami dashboard" src="https://blog.tmk.name/images/analytics/umami.png" /&gt;&lt;/p&gt;
&lt;p&gt;Since upgrading my homelab from an rpi to a proper mini pc, I have been feeling more confident in self hosting, and I am pleased with umami for delivering where cloudflare fell short.&lt;/p&gt;</content><category term="tidbits"></category></entry><entry><title>Django Rest Framework Throttle Race Condition</title><link href="https://blog.tmk.name/2026/02/18/django-rest-framework-throttle-race-condition/" rel="alternate"></link><published>2026-02-18T00:00:00-05:00</published><updated>2026-02-18T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-18:/2026/02/18/django-rest-framework-throttle-race-condition/</id><summary type="html">&lt;p&gt;In a recent project, I used drf's &lt;code&gt;SimpleRateThrottle&lt;/code&gt; to implement rate limiting on some views. Curious about the implementation, I reviewed the source, and noticed that the throttler uses the cache backend with a sliding window log algorithm. The high level approach is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate key for request&lt;/li&gt;
&lt;li&gt;Lookup window in …&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;In a recent project, I used drf's &lt;code&gt;SimpleRateThrottle&lt;/code&gt; to implement rate limiting on some views. Curious about the implementation, I reviewed the source, and noticed that the throttler uses the cache backend with a sliding window log algorithm. The high level approach is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate key for request&lt;/li&gt;
&lt;li&gt;Lookup window in cache by key&lt;/li&gt;
&lt;li&gt;Purge old entries (outside current window)&lt;/li&gt;
&lt;li&gt;Check if window is full&lt;/li&gt;
&lt;li&gt;If so, fail&lt;/li&gt;
&lt;li&gt;Otherwise, insert current timestamp into window&lt;/li&gt;
&lt;li&gt;Set window in cache&lt;/li&gt;
&lt;li&gt;Pass&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice anything? There's a race condition between steps 2 and 7: multiple requests may retrieve the window from the cache in parallel, and later when setting the updated window, the last request will overwrite the other entries. To demonstrate this behavior, I created a small tool to visualize the success rates of requests against a rate limited url:&lt;/p&gt;
&lt;p&gt;&lt;img alt="requests succeeding when they shouldn't" src="https://blog.tmk.name/images/rlui/simplethrottle.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/tristanmkernan/rlui"&gt;rlui&lt;/a&gt; tests requests against a url for success, over time at a given rate. In the above, it's clear that &lt;code&gt;SimpleRateThrottle&lt;/code&gt; is vulnerable to the above race condition, as requests repeatedly exceed the configured limit of 10 req/s: at an input of 40 req/s, the peak success was 38 req/s, with an average success of 24 req/s.&lt;/p&gt;
&lt;p&gt;Now, I'm not the first to point this out - it's &lt;a href="https://github.com/encode/django-rest-framework/issues/5181"&gt;been reported before&lt;/a&gt;. I agree with the maintainers that it's out of scope of drf to provide a perfect solution, as rate limiting algorithms are diverse in their costs and benefits. Out of curiosity, I decided to implement my own rate limiting algorithm, using redis: the same sliding window log algorithm, without the race condition.&lt;/p&gt;
&lt;p&gt;Redis provides a sorted set primitive, which maps each set entry to a score, which is used for ranking or sorting. To implement a sliding window log, map each request key to its numeric timestamp. Redis provides a command to cull the sorted set within a given range, perfect for purging entries outside the current window. At a high level:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate key for request&lt;/li&gt;
&lt;li&gt;Purge old entries (outside current window)&lt;/li&gt;
&lt;li&gt;Add current timestamp into window&lt;/li&gt;
&lt;li&gt;Check if window is full&lt;/li&gt;
&lt;li&gt;If so, remove current timestamp from window, and fail&lt;/li&gt;
&lt;li&gt;Pass&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here's how it runs:&lt;/p&gt;
&lt;p&gt;&lt;img alt="requests not succeeding when they shouldn't" src="https://blog.tmk.name/images/rlui/redisthrottle.png" /&gt;&lt;/p&gt;
&lt;p&gt;The rate limiter never allows requests above 10 req/s. Voila!&lt;/p&gt;
&lt;p&gt;The key difference is that the window updates do not overwrite each other, i.e. entries may be safely added and removed in parallel. The window is therefore not prone to data loss, as in &lt;code&gt;SimpleRateThrottle&lt;/code&gt;. In code, it looks like this:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;now&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;timer()&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;req_key&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFD173"&gt;str&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(now)&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;pipeline&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;redisclient&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;pipeline()&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;pipeline&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;zremrangebyscore(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;key,&lt;/span&gt; &lt;span style="color: #DFBFFF"&gt;0&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;now&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;-&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;duration)&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;pipeline&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;zadd(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;key,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{req_key:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;now})&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;pipeline&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;expire(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;key,&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;duration)&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;pipeline&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;zcard(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;key)&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;_,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;_,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;_,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;num_requests_in_window&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;pipeline&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;execute()&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;num_requests_in_window&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;num_requests:&lt;/span&gt;
    &lt;span style="color: #7e8aa1"&gt;# remove key, request was not processed and doesnt count&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;redisclient&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;zrem(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;key,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;req_key)&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;False&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The relevant redis commands are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;zadd: add an element (and its score) to a sorted set. Used to add requests to the sliding window.&lt;/li&gt;
&lt;li&gt;zcard: measure the cardinality, or size, of the set. Used to count the number of requests in the sliding window.&lt;/li&gt;
&lt;li&gt;zremrangebyscore: removes entries from a set by score. Used to purge old entries in the sliding window, from 0 (beginning of time) to the start of the current window (now - duration).&lt;/li&gt;
&lt;li&gt;expire: expire a key. Used to cleanup the database; after duration seconds, the sliding window may be purged.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note the use of a pipeline, which is similar to a database transaction: this sends all commands together to be executed atomically. This ensures that the window size is counted correctly, and also prevents crashing midway from preventing expiring the key.&lt;/p&gt;
&lt;p&gt;A savvy reader will note that this code has its own race condition: there's a time gap between adding a request to the window and checking the window's size, and later removing the request from the window. If a request comes in and exceeds the window limit, then before that request is removed, a request could come in that should pass, but doesn't, because the window has extra requests. In short, this implementation will reject more requests under heavy pressure, as opposed to allowing them. This could actually be resolved too, but would require a lua script implementing the same logic. Following the drf authors, I wanted a solution that held up, not one that's perfect.&lt;/p&gt;
&lt;p&gt;For the full code of the throttler, see &lt;a href="https://gist.github.com/tristanmkernan/cfafcf2fadf7e6344cf5e7e79144dc37"&gt;this gist&lt;/a&gt;.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Split Tunneling with Network Interfaces</title><link href="https://blog.tmk.name/2026/02/17/split-tunneling-with-network-interfaces/" rel="alternate"></link><published>2026-02-17T00:00:00-05:00</published><updated>2026-02-17T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-17:/2026/02/17/split-tunneling-with-network-interfaces/</id><summary type="html">&lt;p&gt;In setting up my new homelab server, I ran across a networking issue: my cloudflare tunnels were regularly failing when connected through my vpn. The failing network architecture:&lt;/p&gt;
&lt;div class="mermaid"&gt;architecture-beta

    group homelab(server)[Homelab]
    service qbt(server)[Qbittorrent] in homelab
    service cft(server)[Cloudflare Tunnel] in homelab
    service app1(server)[Application …&lt;/div&gt;</summary><content type="html">&lt;p&gt;In setting up my new homelab server, I ran across a networking issue: my cloudflare tunnels were regularly failing when connected through my vpn. The failing network architecture:&lt;/p&gt;
&lt;div class="mermaid"&gt;architecture-beta

    group homelab(server)[Homelab]
    service qbt(server)[Qbittorrent] in homelab
    service cft(server)[Cloudflare Tunnel] in homelab
    service app1(server)[Application 1] in homelab
    service app2(server)[Application 2] in homelab

    service vpn(internet)[VPN]
    service pub(internet)[Public internet]
    junction jleft in homelab
    junction jcenter in homelab
    junction jright in homelab
    junction cfj in homelab

    qbt:T -- B:jleft
    jleft:R -- L:jcenter
    jcenter:T -- B:vpn
    cft:T -- B:jright
    jright:L -- R:jcenter
    vpn:T -- B:pub
    cfj:T -- B:cft
    app1:R -- L:cfj
    app2:L -- R:cfj&lt;/div&gt;
&lt;p&gt;I experimented with different vpn endpoints, but each new endpoint would reliably fail after a short time. Clearly cloudflare is blocking connections through the vpn. Disabling the vpn was out of the question, so I researched different approaches. Split tunneling is afforded by vpns in wireguard via &lt;code&gt;AllowedIPs&lt;/code&gt;, but that only works if the destination IPs are known and stable - which in p2p they are not. I needed to split tunnel based on the application itself, as in the following network diagram:&lt;/p&gt;
&lt;div class="mermaid"&gt;architecture-beta

    group homelab(server)[Homelab]

    service qbt(server)[Qbittorrent] in homelab
    service cft(server)[Cloudflare Tunnel] in homelab
    service app1(server)[Application 1] in homelab
    service app2(server)[Application 2] in homelab

    service vpn(internet)[VPN]
    service pub(internet)[Public internet]

    junction cfj in homelab

    qbt:T -- B:vpn
    cft:T -- B:pub

    cfj:T -- B:cft

    app1:R -- L:cfj
    app2:L -- R:cfj&lt;/div&gt;
&lt;p&gt;Claude suggested a fairly complicated solution, involving isolated networks and bridges. I demurred and searched for something sleek and simple, and found it in the qbittorrent settings: binding the application to a specific network interface, in this case the vpn network interface. I had actually enabled this before, in order to cut off network access when the vpn is down. The missing piece of the puzzle was in the wireguard configuration: setting &lt;code&gt;Table = off&lt;/code&gt; disables wireguard cli from configuring automatic network route binding, in other words from automatically routing all traffic through the vpn interface. Applications may still route traffic through the vpn network interface by explicitly specifying it via the &lt;code&gt;SO_BINDTODEVICE&lt;/code&gt; socket option.&lt;/p&gt;
&lt;p&gt;Not all applications support binding to a particular network interface, so this won't always work; when it does, as in this case with qbittorrent, it makes for trivial split tunneling.&lt;/p&gt;</content><category term="tidbits"></category></entry><entry><title>Migrating a Static Site from S3 to R2</title><link href="https://blog.tmk.name/2026/02/13/migrating-a-static-site-from-s3-to-r2/" rel="alternate"></link><published>2026-02-13T00:00:00-05:00</published><updated>2026-02-13T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-13:/2026/02/13/migrating-a-static-site-from-s3-to-r2/</id><summary type="html">&lt;p&gt;I host a few static sites (including this blog!), and have journeyed from self hosting on a raspberry pi through to cloud hosting. I migrated to S3 static site hosting, which besides the arcane setup steps is otherwise cheap and easy to maintain. Using cloudflare for dns, though, I had …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I host a few static sites (including this blog!), and have journeyed from self hosting on a raspberry pi through to cloud hosting. I migrated to S3 static site hosting, which besides the arcane setup steps is otherwise cheap and easy to maintain. Using cloudflare for dns, though, I had to downgrade the SSL/TSL encryption level, as S3 and cloudflare do not "play nice" together when fronting a bucket with a domain. I decided to move my static sites to R2, cloudflare's object storage offering to restore the encryption level.&lt;/p&gt;
&lt;p&gt;Creating a bucket in R2 is simple enough (once one finds the feature in cloudflare's confusing dashboards). From there, head to settings and add a custom domain. Cloudflare will automatically create or convert the relevant DNS record.&lt;/p&gt;
&lt;p&gt;For upload, the aws cli supports an &lt;code&gt;--endpoint-url &amp;lt;url&amp;gt;&lt;/code&gt; param to interface with other providers (e.g. minio, garage, backblaze, R2, etc.). The makefile command to publish this blog is:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;aws&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;s3&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;sync&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;$(&lt;/span&gt;OUTPUTDIR&lt;span style="color: #FFAD66"&gt;)&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;/&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;s3://&lt;span style="color: #FFAD66"&gt;$(&lt;/span&gt;R2_BUCKET&lt;span style="color: #FFAD66"&gt;)&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--endpoint-url&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;$(&lt;/span&gt;R2_URL&lt;span style="color: #FFAD66"&gt;)&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--delete
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;where R2_BUCKET is the bucket name and R2_URL is your S3 compatible URL (it'll look like https://...long string....r2.cloudflarestorage.com). In the R2 dashboard, I generated S3 API keys for use with aws cli; I use &lt;code&gt;direnv&lt;/code&gt; to dynamically populate my shell environment variables, there I configured &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With the bucket created, dns setup, and content uploaded, your public site should be accessible. The first thing you may notice is that root traffic errors out, e.g. &lt;code&gt;blog.tmk.name/&lt;/code&gt; does not serve the blog. It turns out that R2 does not support a "bucket root" item like S3 static site hosting, in other words there's no built-in feature to use &lt;code&gt;index.html&lt;/code&gt; as the root page.&lt;/p&gt;
&lt;p&gt;Not to worry, cloudflare has a rules feature that solves this pain point. Create a new rule, specifying a custom filter expression:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;URI Path -&amp;gt; equals -&amp;gt; /&lt;/li&gt;
&lt;li&gt;AND&lt;/li&gt;
&lt;li&gt;Hostname -&amp;gt; equals -&amp;gt; blog.tmk.name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rewrite to -&amp;gt; Static -&amp;gt; index.html&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... and save. This will implicitly redirect traffic to &lt;code&gt;blog.tmk.name/&lt;/code&gt; to serve &lt;code&gt;index.html&lt;/code&gt;. For nested static sites, it's possible to configure the page rule to dynamically serve each folder's index.html - see &lt;a href="https://community.cloudflare.com/t/index-html-as-root-object-for-spa/581177"&gt;this helpful community post&lt;/a&gt; for details.&lt;/p&gt;
&lt;p&gt;All in all, it took me about 30 minutes to migrate from S3 to R2, and upgrade my SSL/TSL encryption.&lt;/p&gt;</content><category term="tidbits"></category></entry><entry><title>OSM Ecosystem</title><link href="https://blog.tmk.name/2026/02/12/osm-ecosystem/" rel="alternate"></link><published>2026-02-12T00:00:00-05:00</published><updated>2026-02-12T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-12:/2026/02/12/osm-ecosystem/</id><summary type="html">&lt;p&gt;I've been working on a few projects leveraging the incredible open ecosystem provided by the Open Street Maps network - I want to document a few, because the landscape is quite confusing to a newcomer.&lt;/p&gt;
&lt;h2&gt;Geocoding&lt;/h2&gt;
&lt;p&gt;Geocoding is the process of converting raw, text addresses (e.g. 1600 Pennsylvania Ave.) to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've been working on a few projects leveraging the incredible open ecosystem provided by the Open Street Maps network - I want to document a few, because the landscape is quite confusing to a newcomer.&lt;/p&gt;
&lt;h2&gt;Geocoding&lt;/h2&gt;
&lt;p&gt;Geocoding is the process of converting raw, text addresses (e.g. 1600 Pennsylvania Ave.) to lat/lng geo-coordinates, which are necessary for geospatial applications like viewing on a map, calculating distances, and so on. &lt;a href="https://nominatim.org/"&gt;Nominatim&lt;/a&gt; is built on top of OSM data, and provides a free API (at a low rps limit).&lt;/p&gt;
&lt;p&gt;To call the public API from JS:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;JavaScript&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;let&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; nominatimUrl &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;https://nominatim.openstreetmap.org/search?addressdetails=1&amp;amp;format=jsonv2&amp;amp;limit=1&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;;&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;nominatimUrl &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;+=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;&amp;amp;q=&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;+&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFD173"&gt;encodeURIComponent&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;1600 Pennsylvania Ave&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;);&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;const&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; addressSearchResults &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;await&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; fetch(nominatimUrl).then(res =&amp;gt; res.json());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will return a payload containing geo-coordinates, and other interesting data points sourced from OSM.&lt;/p&gt;
&lt;h2&gt;Point of interest search&lt;/h2&gt;
&lt;p&gt;Points of interest include parks, businesses, museums, libraries, etc. - all the various land uses one finds around the world. OSM contains a rich dataset of crowdsourced data on points of interest. That dataset can be queried by &lt;a href="https://www.overpass-api.de/"&gt;Overpass&lt;/a&gt;, via its &lt;a href="https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_API_by_Example"&gt;API&lt;/a&gt;. Overpass is interfaced with a complex query language (QL), described in the wiki as a "procedural, imperative programming language written with a C style syntax."&lt;/p&gt;
&lt;p&gt;If you are still with me, then take solace that simple queries in Overpass QL are thankfully simple to write. As an example, Finding all cafes in a given bounding box:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;[out:json][timeout:25];
// gather results
node({{bbox}})[&amp;quot;amenity&amp;quot;=&amp;quot;cafe&amp;quot;];
// print results
out geom;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Where &lt;code&gt;{{bbox}}&lt;/code&gt; is a comma separated list of the form: southernmost latitude, westernmost longitude, northernmost latitude, and easternmost longitude. OSM tags include multiple amenity types, including cinemas, bars, libraries, etc.&lt;/p&gt;
&lt;p&gt;Making a request against the API took some figuring out; it seems the approach is to make a POST request against the interpreter endpoint, having encoded the query into the payload with a &lt;code&gt;body=&lt;/code&gt; prefix, as so:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;JavaScript&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;const&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; overpassUrl &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;https://overpass-api.de/api/interpreter&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;;&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;const&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; overpassQuery &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;`&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;[out:json][timeout:25];&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;// gather results&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;node(&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;${&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;overpassBoundingBox&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;}&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;)[&amp;quot;amenity&amp;quot;=&amp;quot;cafe&amp;quot;];&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;// print results&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;out geom;&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;`&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;;&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;const&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; overpassResults &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;await&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; fetch(overpassUrl, {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    method&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    body&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;data=&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;+&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFD173"&gt;encodeURIComponent&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(overpassQuery),&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;}).then(res =&amp;gt; res.json());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will result in potentially a lot of elements returned; each element includes geo-coordinates and a set of tags, which usually (but is not guaranteed to) includes name, address components, etc. Note that the overpass-api.de service is intermittently flaky; there are other free public hosts listed in the project wiki.&lt;/p&gt;
&lt;h2&gt;Routing&lt;/h2&gt;
&lt;p&gt;The traveling salesman problem asks the question, given N cities to visit, what's the optimal order to visit them in, so as to minimize drive time or distance? Generalized, the vehicle routing problem posits multiple vehicles driving multiple routes, looking to visit all locations in the optimal order. To solve these problems, it's necessary to provide driving distance or duration matrices. &lt;a href="https://project-osrm.org/"&gt;OSRM&lt;/a&gt; and &lt;a href="https://valhalla.github.io/valhalla/"&gt;Valhalla&lt;/a&gt; are two (of many) routing-based engines that operate on top of OSM route data (think streets, highways, etc.), providing the distance matrices necessary to solve the vehicle routing problem.&lt;/p&gt;
&lt;p&gt;To generate the distance matrix via OSRM:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;https://router.project-osrm.org/table/v1/driving/-73.9855,40.7580;-74.0134,40.7127;-73.9718,40.7678;-73.9614,40.7143&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Which can then be provided to VRP solvers, e.g. vroom, or-tools, pyvrp, etc.&lt;/p&gt;</content><category term="tidbits"></category></entry><entry><title>Hurl</title><link href="https://blog.tmk.name/2026/02/10/hurl/" rel="alternate"></link><published>2026-02-10T00:00:00-05:00</published><updated>2026-02-10T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-10:/2026/02/10/hurl/</id><summary type="html">&lt;p&gt;Today, in a project, I wanted to set up a few smoke tests to verify that my http api worked in production. &lt;a href="https://hurl.dev/"&gt;hurl&lt;/a&gt; came to the rescue, a simple and straightforward text-based tool (and text does run the world!).&lt;/p&gt;
&lt;p&gt;Here's a sample of my tests file (generated handily by claude …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Today, in a project, I wanted to set up a few smoke tests to verify that my http api worked in production. &lt;a href="https://hurl.dev/"&gt;hurl&lt;/a&gt; came to the rescue, a simple and straightforward text-based tool (and text does run the world!).&lt;/p&gt;
&lt;p&gt;Here's a sample of my tests file (generated handily by claude):&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #7e8aa1"&gt;# --- Auth ---&lt;/span&gt;

&lt;span style="color: #7e8aa1"&gt;# Reject missing auth&lt;/span&gt;
GET&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;{{&lt;/span&gt;api_url&lt;span style="color: #FFAD66"&gt;}}&lt;/span&gt;/osrm/route/v1/driving/-74.172400,40.735700&lt;span style="color: #d4d2c8"&gt;;&lt;/span&gt;-74.027600,40.744000
HTTP&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;401&lt;/span&gt;

&lt;span style="color: #7e8aa1"&gt;# Reject invalid key&lt;/span&gt;
GET&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;{{&lt;/span&gt;api_url&lt;span style="color: #FFAD66"&gt;}}&lt;/span&gt;/osrm/route/v1/driving/-74.172400,40.735700&lt;span style="color: #d4d2c8"&gt;;&lt;/span&gt;-74.027600,40.744000
Authorization:&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;Bearer&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;00000000&lt;/span&gt;-0000-0000-0000-000000000000
HTTP&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;401&lt;/span&gt;

&lt;span style="color: #7e8aa1"&gt;# --- OSRM Route ---&lt;/span&gt;

&lt;span style="color: #7e8aa1"&gt;# Basic route request&lt;/span&gt;
GET&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;{{&lt;/span&gt;api_url&lt;span style="color: #FFAD66"&gt;}}&lt;/span&gt;/osrm/route/v1/driving/-74.172400,40.735700&lt;span style="color: #d4d2c8"&gt;;&lt;/span&gt;-74.027600,40.744000
Authorization:&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;Bearer&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;{{&lt;/span&gt;api_key&lt;span style="color: #FFAD66"&gt;}}&lt;/span&gt;
HTTP&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;200&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;[&lt;/span&gt;Asserts&lt;span style="color: #FFAD66"&gt;]&lt;/span&gt;
header&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Content-Type&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;contains&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;application/json&amp;quot;&lt;/span&gt;
jsonpath&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;$&lt;span style="color: #D5FF80"&gt;.code&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;==&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Ok&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Hurl supports variables, which makes running against different servers (local / prod) a breeze. I configured the variables in my &lt;code&gt;.env&lt;/code&gt; which get auto-populated in my shell by &lt;code&gt;direnv&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #7e8aa1"&gt;## dev&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;HURL_VARIABLE_api_key&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;asdf
&lt;span style="color: #d4d2c8"&gt;HURL_VARIABLE_api_url&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;http://localhost:8000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that environment variables are prefixed with &lt;code&gt;HURL_VARIABLE_&amp;lt;name&amp;gt;&lt;/code&gt;, and interpolated in the hurl file with just &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To run the tests:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;hurl&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;tests/smoke.hurl&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--test&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--error-format&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;long
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will print a summary of the test results, bailing on error with the failing response.&lt;/p&gt;
&lt;p&gt;Browsing the docs, &lt;code&gt;hurl&lt;/code&gt; can be used as just an http automation tool, or in test mode as above, and also as a load/stress test tool with the &lt;code&gt;--repeat &amp;lt;count&amp;gt;&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;I found &lt;code&gt;hurl&lt;/code&gt; to fit the niche in between manual tests and unit tests.&lt;/p&gt;</content><category term="tidbits"></category></entry><entry><title>Hide Category From Index in Pelican</title><link href="https://blog.tmk.name/2026/02/09/hide-category-from-index-in-pelican/" rel="alternate"></link><published>2026-02-09T00:00:00-05:00</published><updated>2026-02-09T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-09:/2026/02/09/hide-category-from-index-in-pelican/</id><summary type="html">&lt;p&gt;Fitting enough for my first TIL post, I had to figure out how exclude posts in the TIL category from my pelican blog, without breaking pagination. The feature has been &lt;a href="https://github.com/getpelican/pelican/issues/3146"&gt;requested in the pelican github&lt;/a&gt;, and thankfully the code snippet at the end has the solution:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;ExcludeWriter&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(Writer …&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;Fitting enough for my first TIL post, I had to figure out how exclude posts in the TIL category from my pelican blog, without breaking pagination. The feature has been &lt;a href="https://github.com/getpelican/pelican/issues/3146"&gt;requested in the pelican github&lt;/a&gt;, and thankfully the code snippet at the end has the solution:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;ExcludeWriter&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(Writer):&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;write_file&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;
        &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;name,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;template,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;context,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;relative_urls&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=False&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;paginated&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=None&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;template_name&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=None&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;override_output&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=False&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;url&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=None&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;**&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;kwargs&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;):&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;articles&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;kwargs&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;get(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;articles&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;page_name&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;kwargs&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;get(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;page_name&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;page_name&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;==&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;#39;index&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
            &lt;span style="color: #7e8aa1"&gt;### set categories to exclude here&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;articles&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;[a&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;a&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;articles&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;not&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;a&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;category&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;==&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;#39;TIL&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;kwargs[&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;articles&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;articles&lt;/span&gt;

        &lt;span style="color: #FFD173"&gt;super&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;()&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;write_file(&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;name&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;name,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;template&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;template,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;context&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;context,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;relative_urls&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;relative_urls,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;paginated&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;paginated,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;template_name&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;template_name,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;override_output&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;override_output,&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;url&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;url,&lt;/span&gt;
            &lt;span style="color: #FFAD66"&gt;**&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;kwargs&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;


&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;get_writer&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(sender):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;ExcludeWriter&lt;/span&gt;


&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;register&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;():&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;&amp;quot;&amp;quot;&amp;quot;Register the new plugin&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;signals&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;get_writer&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;connect(get_writer)&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;register()&lt;/span&gt;  &lt;span style="color: #7e8aa1"&gt;# &amp;lt;-- this line added to the code in the snippet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Place this in &lt;code&gt;pelicanconf.py&lt;/code&gt; and restart the devserver.&lt;/p&gt;</content><category term="tidbits"></category></entry><entry><title>Async Django</title><link href="https://blog.tmk.name/2026/02/01/async-django/" rel="alternate"></link><published>2026-02-01T00:00:00-05:00</published><updated>2026-02-01T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-02-01:/2026/02/01/async-django/</id><summary type="html">&lt;p&gt;For a recent project, I decided to go all-in on async Django in order to get a feel for its advances in recent years. I've long been inspired by the infamous "Erlang replaces your entire stack" slide:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Erlang replaces your entire stack" src="https://blog.tmk.name/images/elixir-does-all.jpeg" /&gt;&lt;/p&gt;
&lt;p&gt;And for this project, I wanted to see what I could replicate with …&lt;/p&gt;</summary><content type="html">&lt;p&gt;For a recent project, I decided to go all-in on async Django in order to get a feel for its advances in recent years. I've long been inspired by the infamous "Erlang replaces your entire stack" slide:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Erlang replaces your entire stack" src="https://blog.tmk.name/images/elixir-does-all.jpeg" /&gt;&lt;/p&gt;
&lt;p&gt;And for this project, I wanted to see what I could replicate with Django and async Python.&lt;/p&gt;
&lt;h2&gt;Project&lt;/h2&gt;
&lt;p&gt;How do I share text snippets (long links, code snippets, log lines, etc.) between phone, laptop, and desktop? To this day, I rely on email and whatsapp, messaging myself. To improve this, I created &lt;a href="https://sps.tmk.name/"&gt;SimplePasteService&lt;/a&gt; (&lt;a href="https://github.com/tristanmkernan/simplepasteservice"&gt;source code&lt;/a&gt;) - a website for quickly sharing text content between devices without long random ids nor the email login bootstrapping problem. For fun, I added live updates: two devices on the same paste will see new text updates stream in.&lt;/p&gt;
&lt;p&gt;The project requirements demanded two technical solutions. First, as pastes expire, they should be deleted in a recurring background task. Second, cross-client communication is necessary for live updates.&lt;/p&gt;
&lt;p&gt;For a typical sync Django project, I'd solve the former with celery and celery beat. This adds two additional processes to run and manage, in addition to the message broker and result store. For the latter, I'd lean on a pubsub server, possibly a SaaS solution. The complexity jumps for each feature are out of proportion, in my view.&lt;/p&gt;
&lt;p&gt;What I'd like is to keep complexity low. Could I replace my stack with asyncio?&lt;/p&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The short answer is yes, but with caveats. The core limitation is that async Python is a single process, and single threaded, as opposed to the Erlang/BEAM, which runs across all cores, and can be extended to run across multiple machines. As this is a hobby project, I made the decision early to accept this limitation: I'd deploy a single process with a single thread to run the webserver, pubsub, cron, and background tasks. This is not a scalable solution, clearly, but for a low-traffic hobby project, the reduced complexity is a compelling benefit.&lt;/p&gt;
&lt;p&gt;Before diving into these components, one more architectural decision had to be made. For live streaming updates, I decided on server sent events, a simple push-only protocol that works mostly out of the box. To implement, I leveraged htmx's sse extension for the frontend, and Django's &lt;code&gt;StreamingHttpResponse&lt;/code&gt; on the backend.&lt;/p&gt;
&lt;h3&gt;Webserver&lt;/h3&gt;
&lt;p&gt;Django comes out of the box with asgi support; all that's needed is a compatible webserver. I chose the popular &lt;code&gt;uvicorn&lt;/code&gt;. For development, I used the following invocation:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;uvicorn&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;simplepasteservice.asgi:application&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--port&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;8000&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;  &lt;/span&gt;--reload&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;  &lt;/span&gt;--reload-include&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;*.html&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;  &lt;/span&gt;--timeout-graceful-shutdown&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note &lt;code&gt;--timeout-graceful-shutdown&lt;/code&gt;: I noticed when developing with long lived sse connections that the server would not automatically restart until the connections closed. This parameter will close those connections automatically after the grace period. I also added html files to the reloader, so that template updates would automatically refresh.&lt;/p&gt;
&lt;h3&gt;Background tasks and cron&lt;/h3&gt;
&lt;p&gt;To replace celery and beat, I decided on apscheduler, which has an asyncio-friendly scheduler, and is easy to set up. I updated &lt;code&gt;asgi.py&lt;/code&gt; to add:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;scheduler&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;AsyncIOScheduler()&lt;/span&gt;

&lt;span style="color: #7e8aa1"&gt;# also possible to use string &amp;quot;simplepasteservice.tasks:delete_expired_pastes&amp;quot;&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;scheduler&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;add_job(delete_expired_pastes,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;trigger&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;IntervalTrigger(hours&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;1&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;))&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;scheduler&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;start()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And with just these few lines, I had my desired recurring background task. One-off immediate-run tasks need no special setup when queued from an async view, look at &lt;a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task"&gt;asyncio.create_task()&lt;/a&gt; - but I didn't have a need for such tasks in this project.&lt;/p&gt;
&lt;h3&gt;Pubsub&lt;/h3&gt;
&lt;p&gt;The basic feature here was: when a paste is updated with new content, ping the clients subscribed. As the application is just a single event loop, I decided to build a trivial pubsub server within the application itself. The core data structure is a dictionary mapping pastes to subscriber queues:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;paste_update_listeners&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;defaultdict(&lt;/span&gt;&lt;span style="color: #FFD173"&gt;list&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;async&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;view_paste_sse&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(request,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;key):&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;&amp;quot;&amp;quot;&amp;quot;Creates and listens on an update queue, pinging client on updates&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;global&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;paste_update_listeners&lt;/span&gt;

    &lt;span style="color: #7e8aa1"&gt;# ensure paste exists&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;paste&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;await&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;aget_object_or_404(Paste&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;objects&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;unexpired(),&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;key&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;key)&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;q&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;asyncio&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;Queue()&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;paste_update_listeners[key]&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;append(q)&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;async&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;event_stream&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;():&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;try&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
            &lt;span style="color: #FFAD66"&gt;while&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;True&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
                &lt;span style="color: #FFAD66"&gt;await&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;q&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;get()&lt;/span&gt;

                &lt;span style="color: #7e8aa1"&gt;# update occurred!&lt;/span&gt;
                &lt;span style="color: #FFAD66"&gt;yield&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;event: update&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\n&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;data:&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\n\n&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;

        &lt;span style="color: #FFAD66"&gt;except&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;asyncio&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;CancelledError:&lt;/span&gt;
            &lt;span style="color: #7e8aa1"&gt;# connection closed, clean up&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;paste_update_listeners[key]&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;remove(q)&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;q&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;shutdown()&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;StreamingHttpResponse(&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;event_stream(),&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;content_type&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;text/event-stream&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When pastes are updated, all clients are notified:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;async&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;create_paste_item&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(request,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;key):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;global&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;paste_update_listeners&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;paste&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;await&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;aget_object_or_404(Paste&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;objects&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;unexpired(),&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;key&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;key)&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;form&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;CreatePasteItemForm(request&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;POST,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;paste&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;paste)&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;form&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;is_valid():&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;await&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;PasteItem&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;objects&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;acreate(paste&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;paste,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;text&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;form&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;cleaned_data[&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;])&lt;/span&gt;

        &lt;span style="color: #7e8aa1"&gt;# TODO: serial processing here could be made async&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;listener_q&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;paste_update_listeners[key]:&lt;/span&gt;
            &lt;span style="color: #FFAD66"&gt;await&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;listener_q&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;put(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;bump&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

        &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;redirect(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;view_paste&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;paste&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;key)&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;messages&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;error(request,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;Error adding paste item&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;redirect(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;index&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Nice. In a few dozen lines, live streaming cross client updates, all in async Django!&lt;/p&gt;
&lt;h2&gt;Pitfalls&lt;/h2&gt;
&lt;p&gt;I am overall quite pleased with the final result, having replaced several elements of my typical web application stack with async Django. That said, asyncio feels off, suffering from the &lt;a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/"&gt;function coloring problem&lt;/a&gt; that took several headaches to imprint &lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;. Django suffers from the same, and it feels like async support is still some distance away from prime time.&lt;/p&gt;
&lt;h3&gt;Async Django&lt;/h3&gt;
&lt;p&gt;I set up my project entirely along async lines: async views, async-compatible middleware. This wasn't strictly necessary, but I wanted to get a feel for the state of an all async project. For views, what bit me was that template rendering is not async friendly. It will work, up until async code is called, or in an async view, if any "synchronous only" code is called. The recommended solution is to wrap template rendering in &lt;code&gt;sync_to_async&lt;/code&gt; and call it a day; as an async zealot for the day, I refused this approach, and instead accessed my async database lookups in the view in painstaking fashion.&lt;/p&gt;
&lt;p&gt;In my research, I discovered Jinja2 does support async rendering, even leveraging incremental html generation to stream the results to the client. I'd take async-compatible Django template rendering without the bells and whistles, though the prospect of colored templates (either async or non-async) makes me nervous.&lt;/p&gt;
&lt;p&gt;The middleware story is also sub-par: if any middleware is non-async friendly, then the entire request processing flow becomes non-async, which means there goes my async processing! For this project, I had few third party middleware, but sadly whitenoise tripped the async unfriendly wire, and had to be replaced with blacknoise. This alternative is set up on top of the asgi application, rather than as a django middleware:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;application&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;BlackNoise(get_asgi_application())&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;application&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;add(BASE_DIR&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;/&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;staticfiles&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;/static&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I don't know if this is a technical limitation (perhaps due to file streaming in async?), but I'd have much preferred the standard Django middleware integration.&lt;/p&gt;
&lt;h3&gt;SSE&lt;/h3&gt;
&lt;p&gt;The server sent events protocol is not perfect, but suffices for a project of this scope. As described above, I leveraged Django's &lt;code&gt;StreamingHttpResponse&lt;/code&gt; with an async streaming generator to send events down the wire. In my homelab deployment with Caprover, I noticed that updates were not coming through; this is due to nginx reverse proxying not streaming sse appropriately by default, the following header instructed nginx to play nice:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;StreamingHttpResponse(&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;event_stream(),&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;content_type&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;text/event-stream&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;headers&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;{&lt;/span&gt;
        &lt;span style="color: #7e8aa1"&gt;# for nginx proxying sse support&lt;/span&gt;
        &lt;span style="color: #D5FF80"&gt;&amp;quot;X-Accel-Buffering&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;no&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;},&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The other major issue I ran into involved stuffing html into sse event payloads. I first attempted to configure htmx to stream the html from sse events directly into the dom, but ran into a problem with the sse protocol: newline sensitivity. As Django renders templates with many newlines, I attempted to simply remove them:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt; &lt;span style="color: #7e8aa1"&gt;# rendered html has to be stripped of newlines, sse format limitation&lt;/span&gt;
 &lt;span style="color: #7e8aa1"&gt;# see https://github.com/bigskysoftware/htmx/issues/2292&lt;/span&gt;
 &lt;span style="color: #7e8aa1"&gt;# ah.. but then the copy-paste wont work with newlines... :(&lt;/span&gt;
 &lt;span style="color: #d4d2c8"&gt;html&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;render_to_string(&lt;/span&gt;
     &lt;span style="color: #D5FF80"&gt;&amp;quot;paste_detail.html#paste-items-list&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
     &lt;span style="color: #d4d2c8"&gt;{&lt;/span&gt;
         &lt;span style="color: #D5FF80"&gt;&amp;quot;pasteitems&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;pasteitems,&lt;/span&gt;
     &lt;span style="color: #d4d2c8"&gt;},&lt;/span&gt;
 &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

 &lt;span style="color: #d4d2c8"&gt;html&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;html&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;replace(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\n&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;replace(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\r&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

 &lt;span style="color: #FFAD66"&gt;yield&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;event: update&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\n&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;
 &lt;span style="color: #FFAD66"&gt;yield&lt;/span&gt; &lt;span style="color: #F29E74"&gt;f&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;data: &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;{&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;html&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;}\n\n&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This would have worked, &lt;em&gt;except&lt;/em&gt; that I had newline sensitive code in the templates. Suppose a user inputs a text with newlines (eg paragraphs or log lines). To implement a "copy on click" feature to the page, I added a &lt;code&gt;data-copyable="{{ content }}"&lt;/code&gt; attribute to relevant elements, and an event listener that copied the attribute content into the clipboard. That inner content could include newlines that could not simply be stripped out without breaking the copy feature &lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Finding myself limited by the protocol, I pursued a simpler approach: on sse message, make a request to a view to refresh the template partial. While an extra round trip to the server, it fit in more cleanly and saved the headache. I'd advocate that htmx add support for a friendlier encoding scheme, to support html over sse.&lt;/p&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;While async in Python and Django has come a long way, I would not choose this stack for a production project. For a hobby project, it's impressive how much of a typical web stack can be eliminated; I think this could be a boon for solo and aspiring devs on side projects. I plan to check out some of the newer async-first kids on the Python-web-framework block, but I know I'll miss Django's batteries.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;For instance, Python coroutines do not run unless await'd or explicitly scheduled, unlike Javascript promises, which are automatically scheduled.&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Perhaps the content could have been JSON encoded in the template, and parsed in the Javascript.&amp;#160;&lt;a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="programming"></category></entry><entry><title>Security Stories Part 4</title><link href="https://blog.tmk.name/2026/01/19/security-stories-part-4/" rel="alternate"></link><published>2026-01-19T00:00:00-05:00</published><updated>2026-01-19T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-01-19:/2026/01/19/security-stories-part-4/</id><summary type="html">&lt;p&gt;I am back once again with another entry from my archive of security stories. Once more, the purpose is education: as an engineer, it pays dividends to &lt;a href="https://blog.tmk.name/2025/10/23/scratch-the-itch/"&gt;be curious and think critically&lt;/a&gt; about the software one is writing and maintaining.&lt;/p&gt;
&lt;h2&gt;Token interchange&lt;/h2&gt;
&lt;p&gt;One company I worked for leveraged magic links …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I am back once again with another entry from my archive of security stories. Once more, the purpose is education: as an engineer, it pays dividends to &lt;a href="https://blog.tmk.name/2025/10/23/scratch-the-itch/"&gt;be curious and think critically&lt;/a&gt; about the software one is writing and maintaining.&lt;/p&gt;
&lt;h2&gt;Token interchange&lt;/h2&gt;
&lt;p&gt;One company I worked for leveraged magic links: the "click this link to log in" in an email that signs you in without needing to provide your password. The security basis for these links is the presumption that access to email justifies access to external accounts using the email. After all, if I control the email for &lt;code&gt;victim@example.com&lt;/code&gt;, to access their account on &lt;code&gt;foo.com&lt;/code&gt; I need only reset their password &lt;em&gt;via email&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Let's set up a scenario so that the exploit path may be described: our imaginary company develops a job board, where organizations post job listings, and candidates apply, with their applications managed within the job board itself. The company decided to improve the candidate experience through magic links. To log in, an applicant is sent an email with a magic login link. After applying, an applicant is sent a magic link to access, update, or withdraw the application.&lt;/p&gt;
&lt;p&gt;I had been suspicious of these magic links for some time, as any security minded engineer should be wary of non-standard authentication schemes. It was in reviewing a new feature related to magic links that I realized a core insecurity. First, some basic background about the implementation of magic links. They are typically of the form:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;https://foo.com/login/magic/&amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;where &lt;code&gt;&amp;lt;token&amp;gt;&lt;/code&gt; may be a randomly generated string mapping to a stored object (think cache or db), or a hash of an id (think hmac of user id), or an encoding of an id (think encrypting the id), or a combination (think jwt). This company followed the encoded id approach, that is, the &lt;code&gt;&amp;lt;token&amp;gt;&lt;/code&gt; translated to an object id.&lt;/p&gt;
&lt;p&gt;In studying the new feature, I made the connection that tokens could be used interchangeably between the two magic link systems, because the &lt;em&gt;ids were not namespaced&lt;/em&gt;, i.e. encoded alongside the resource type. That meant a token to login user 1 could be used to access job application 1, and &lt;em&gt;vice versa&lt;/em&gt;: an application magic link token for application 72 could be used to login as user 72.&lt;/p&gt;
&lt;p&gt;The exploit lended itself to recursive scraping:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;create a new account, user N&lt;/li&gt;
&lt;li&gt;use user id N token to access application N&lt;/li&gt;
&lt;li&gt;access application N owner account, user M&lt;/li&gt;
&lt;li&gt;repeat step 2 with M&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Given that an attacker may create many new user accounts, this means mass account takeover and data scraping. Ouch!&lt;/p&gt;
&lt;p&gt;The key to securing these magic links was, as mentioned, namespacing the ids to prevent re-use. This looked like encoding the resource type alongside the object, in effect encoding the tuple &lt;code&gt;(object id, resource type)&lt;/code&gt;. For the user authentication tokens, that meant &lt;code&gt;(&amp;lt;user id&amp;gt;, "user::login")&lt;/code&gt;, and for the application tokens, &lt;code&gt;(&amp;lt;application id&amp;gt;, "application::crud")&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Each consumer of tokens would check to ensure that the input token was of the appropriate resource to access the underlying functionality; e.g. the &lt;code&gt;TokenLoginView&lt;/code&gt; would check that the token was of type &lt;code&gt;"user::auth"&lt;/code&gt; before authenticating the given user.&lt;/p&gt;
&lt;p&gt;As the magic links were part of a live system, with existing magic links in play, the migration from the old to the new token schema was a key challenge. In effect, the new token schema was rolled out alongside support for the previous, itself removed after an agreed-upon expiration timeline.&lt;/p&gt;
&lt;p&gt;This vulnerability underlined the importance of the common phrase, "don't roll your own crypto." Here I'd amend that phrase to "don't roll your own authentication." As with all such phrases, it's not a commandment, but intended to share the common wisdom of warnings from the generations past that "here be dragons" when going down such paths.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Security Stories Part 3</title><link href="https://blog.tmk.name/2026/01/16/security-stories-part-3/" rel="alternate"></link><published>2026-01-16T00:00:00-05:00</published><updated>2026-01-16T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-01-16:/2026/01/16/security-stories-part-3/</id><summary type="html">&lt;p&gt;I am back to share another security story from my archive. In addition to the pleasure of sharing my experiences, I write so that security may be taken more seriously by software organizations. It's all too easy to &lt;code&gt;__dangerouslySetInnerHtml&lt;/code&gt; and call it a day; it takes active intent and investment …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I am back to share another security story from my archive. In addition to the pleasure of sharing my experiences, I write so that security may be taken more seriously by software organizations. It's all too easy to &lt;code&gt;__dangerouslySetInnerHtml&lt;/code&gt; and call it a day; it takes active intent and investment to secure software systems.&lt;/p&gt;
&lt;h2&gt;Rogue templates&lt;/h2&gt;
&lt;p&gt;At one company I worked for, there was a vast array of legacy internal software. One particular piece of internal glue code had caught my eye: using the django templating engine over user provided inputs. To avoid particulars, let's say, for example, that the relevant feature was built to send an email each time a user registers. A user registers for the platform with a name and profile blurb, which should be included in the email to admins.&lt;/p&gt;
&lt;p&gt;Now, the secure way to do this would be to create an email template:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;{% include &amp;quot;email_header.txt&amp;quot; %}

User {{ username }} registered! Their profile blurb is:

{{ blurb }}

{% include &amp;quot;email_footer.txt&amp;quot; %}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and render it, using context to safely interpolate user input:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django.template&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;engines&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;user&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;User(username&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;tmoney&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;blurb&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Happy to be here&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;django_engine&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;engines[&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;django&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;template&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django_engine&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;get_template(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;user_registration_email.txt&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;rendered&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;template&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;render({&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;username&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;user&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;username,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;blurb&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;user&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;blurb})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Suppose that a user provided a malicious blurb. At first, thinking about the template engine, I figured the worst would be a denial of service attack via resource exhaustion, e.g.&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;What I hear on the news:

{% for i in &amp;quot;x&amp;quot;|rjust:&amp;quot;10&amp;quot; %}
  .. repeat for loops ..
  blah
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the safe rendering approach, the template code in this blurb would not be executed, just safely output as-is into the email. This wouldn't be a security story if the company had used the safe approach, of course! Instead, the email templating code was written to include the blurb as part of the raw template, as in:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django.template&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;engines&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;user&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;User(username&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;tmoney&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;blurb&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Happy to be here&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;django_engine&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;engines[&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;django&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;template&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django_engine&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;from_string(&lt;/span&gt;&lt;span style="color: #F29E74"&gt;f&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;    &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;{{&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;% include &amp;quot;email_header.txt&amp;quot; %&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;}}&lt;/span&gt;

&lt;span style="color: #D5FF80"&gt;    User &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;{&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;user&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;username&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;}&lt;/span&gt;&lt;span style="color: #D5FF80"&gt; registered! Their profile blurb is:&lt;/span&gt;

&lt;span style="color: #D5FF80"&gt;    &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;{&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;user&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;blurb&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;}&lt;/span&gt;

&lt;span style="color: #D5FF80"&gt;    &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;{{&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;% include &amp;quot;email_footer.txt&amp;quot; %&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;}}&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;rendered&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;template&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;render()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With this approach, the malicious blurb template code is executed by the template engine. Assuming that the worst that could be achieved is resource exhaustion, I filed this away as a low priority vulnerability. But it kept creeping up in my mind... knowing that django templates can call methods, and thinking about the django orm, if I could get a reference to a django model then I could easily chain that model to related models or possibly the entire table via model querysets.&lt;/p&gt;
&lt;p&gt;After more digging, I finally found it: &lt;code&gt;get_admin_log&lt;/code&gt;. This is automatically available in django templates when the admin site is enabled and vulnerable when the admin site is used - and you are using it, right, that's part of the django batteries included deal! &lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;This template tag queries recent admin actions (specifically, the &lt;code&gt;LogEntry&lt;/code&gt; model), think creations, edits, deletions. Handily, it links to the user who made the change, and that user is presumably a superuser, and presumably has links to most other objects in the system... now we are cooking!&lt;/p&gt;
&lt;p&gt;Let's think through some of the possibilities for exploitation. With access to any &lt;code&gt;User&lt;/code&gt; object, it's easy to access all user objects, via django meta model magic: &lt;code&gt;user._meta.model.objects.all()&lt;/code&gt;. From there, one may head straight for deleting objects, including following the model meta magic train to related models and wiping much of the database.&lt;/p&gt;
&lt;p&gt;Fortunately, django is ahead of us on this front. First, "private" properties beginning with underscores are not accessible in template lookups. Second, common side-effect methods like &lt;code&gt;queryset.delete()&lt;/code&gt; or &lt;code&gt;model.delete()&lt;/code&gt; are marked with a special flag that prevent execution in template rendering (via &lt;code&gt;alters_data&lt;/code&gt;, see &lt;a href="https://docs.djangoproject.com/en/6.0/ref/templates/api/#variables-and-lookups"&gt;the docs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;That excludes the most obvious exploit of wiping tables via the standard django orm. It doesn't exclude rendering data, though, in the case that attackers are able to view the results of the injected template code. And, given the relative obscurity of template injection protection (blacklist unsafe methods, rather than whitelist safe methods), it doesn't exclude developer defined model methods. It's common to define helper methods that overlay the very same side-effect methods like &lt;code&gt;model.delete()&lt;/code&gt;, and such helper methods would be executable within the template.&lt;/p&gt;
&lt;p&gt;Let's continue with the example, supposing that there's a custom method &lt;code&gt;User.deactivate()&lt;/code&gt;, that is typically run when a user deactivates their account. Not thinking about the potential attack vector of template injection, this method is &lt;em&gt;not&lt;/em&gt; marked unsafe for templates. Now, an attacker may provide a malicious blurb that deactivates admin users, locking them out of the system:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Nothing to see here :D

{% load log %}
{% get_admin_log 100 as log %}
{% for entry in log %}
  {{ entry.user.deactivate }}
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is just one example. The true attack surface depends on the context provided to the rendering call (e.g. the current user), and the methods available on objects available within the context (whether directly provided or provided via template tags).&lt;/p&gt;
&lt;p&gt;I'd classify this vulnerability as code injection leading to limited remote code execution. What made this stand out to me was the seemingly innocent django templating engine: with a naive mindset of, "it interpolates user inputs when rendering", an engineer misses the potentially catastrophic consequences of running the template engine on unsafe inputs.&lt;/p&gt;
&lt;p&gt;Therefore my main lesson here is to always be mindful of processing untrusted input. Another important lesson is that if it seems insecure, then it's probably insecure - dig in, talk about it, and fix it. Engineering teams should cultivate a positive, blame-free culture to investigate, report, address and educate.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;In Django 5.0+, &lt;code&gt;get_admin_log&lt;/code&gt; fails outside of admin views. This appears to have been (incidentally?) &lt;a href="https://docs.djangoproject.com/en/6.0/releases/5.0/#django-contrib-admin"&gt;patched out&lt;/a&gt;. Note that this is just one, default-provided attack vector. It's common to provide context to render calls, and that context is likely to have access to model objects.&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="programming"></category></entry><entry><title>End of Year Review (2025)</title><link href="https://blog.tmk.name/2025/12/31/end-of-year-review-2025/" rel="alternate"></link><published>2025-12-31T00:00:00-05:00</published><updated>2025-12-31T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-12-31:/2025/12/31/end-of-year-review-2025/</id><summary type="html">&lt;p&gt;December rolls around once more. It's time to reflect on the year gone by, and the year upcoming. This is the &lt;a href="https://blog.tmk.name/2024/12/23/end-of-year-review-2024/"&gt;third installment of my end of year reviews&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This was a year of challenges, struggle, inspiration, burnout, revelation, misery and hope.&lt;/p&gt;
&lt;p&gt;As a friend put it, God gives us …&lt;/p&gt;</summary><content type="html">&lt;p&gt;December rolls around once more. It's time to reflect on the year gone by, and the year upcoming. This is the &lt;a href="https://blog.tmk.name/2024/12/23/end-of-year-review-2024/"&gt;third installment of my end of year reviews&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This was a year of challenges, struggle, inspiration, burnout, revelation, misery and hope.&lt;/p&gt;
&lt;p&gt;As a friend put it, God gives us challenges the moment we're ready to face them.&lt;/p&gt;
&lt;h2&gt;Travel&lt;/h2&gt;
&lt;p&gt;I traveled to Europe twice this year, once for a work + pleasure trip to the UK and Ireland to attend DjangoCon EU, and then a vacation to Greece. I had a whirlwind time in London, trying to squeeze everything into a few short days; old town Edinburgh was rather sham-touristy and dreary; in Ireland I looked out over the Atlantic from the Cliffs of Mohre. In Greece, I walked alongside my ancient heroes, Socrates, Diogenes, Hercules. I traveled independently, facing my fears and making a splendid trip for myself.&lt;/p&gt;
&lt;h2&gt;Books&lt;/h2&gt;
&lt;p&gt;I read 15 books this year. My highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.tmk.name/2025/07/12/thinking-in-systems/"&gt;Thinking in Systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Excession&lt;/li&gt;
&lt;li&gt;Nexus&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Writing&lt;/h2&gt;
&lt;p&gt;I exceeded my goal, writing at least two posts per month. Some posts that I am particularly proud of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Security stories series: &lt;a href="https://blog.tmk.name/2025/03/02/security-stories-part-1/"&gt;part 1&lt;/a&gt;, &lt;a href="https://blog.tmk.name/2025/03/17/security-stories-part-2/"&gt;part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tmk.name/2025/04/06/pytest-playwright-and-django/"&gt;Pytest, playwright and Django&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tmk.name/2025/10/13/visualizing-my-wikipedia-browsing-history/"&gt;Visualizing my Wikipedia Browsing History&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Career&lt;/h2&gt;
&lt;p&gt;I am ending the year having resigned from my job, with the aim to take several months break. I have plenty to write on the experience, so suffice it to say that the greatest surprise for me is the boldness and courage I demonstrated in taking this chance for myself to spend time with myself, get to know myself, and care for myself. In the words of Lester Burnham:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It's a great thing when you realize you still have the ability to surprise yourself. Makes you wonder what else you can do that you've forgotten about.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Personal development&lt;/h2&gt;
&lt;p&gt;This continued to be my greatest theme: at one point, I estimated I have been spending about 15 hours a week between journaling, therapy, yoga, meditation, and support groups. Some highlights of this year's achievements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navigating the end of close relationships&lt;/li&gt;
&lt;li&gt;Avoiding getting into unhealthy relationships&lt;/li&gt;
&lt;li&gt;Developing new, healthy relationships&lt;/li&gt;
&lt;li&gt;Setting boundaries, especially at work&lt;/li&gt;
&lt;li&gt;Deep parts work&lt;/li&gt;
&lt;li&gt;Nurturing self love and compassion&lt;/li&gt;
&lt;li&gt;Decorating my apartment&lt;/li&gt;
&lt;li&gt;Yoga class&lt;/li&gt;
&lt;li&gt;Volunteering at a rehab center&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So much of this work pays dividends in hard to measure ways; without direct comparison to the non-recovered version of myself in the same scenario, I have to validate myself in my new behaviors. Many times I am even taking the same action, but it's my relationship to myself and the behavior that has changed.&lt;/p&gt;
&lt;h2&gt;Goals&lt;/h2&gt;
&lt;p&gt;Looking back, I hit many of my goals from last year, notably:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Physical health (joined a yoga class)&lt;/li&gt;
&lt;li&gt;✅ Career (blogging regularly, conference attendance)&lt;/li&gt;
&lt;li&gt;✅ Relationships (nurturing supportive community)&lt;/li&gt;
&lt;li&gt;✅ Travel (multiple international trips)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the upcoming year, I feel less compelled to make a list of specific targets. On reflection, this has been my trend: from a very long list, to a short, thematic list, to a more flexible approach. I've learned, and proven to myself many times over, that I succeed at strict task-oriented projects. My next challenge is to learn to navigate and live in ambiguity. Specifically, and quite generically, I want to practice living with intention. So much of my default behaviors tend towards productivity-, resource-, or task-oriented mindsets. As a practical example, rather than starting a Saturday with a todo list, I want to check in with myself throughout the day, and act in a way that honors myself, honors the "God of loving kindness", not the "God of productivity."&lt;/p&gt;
&lt;p&gt;To that end, I want to trial the following behavioral changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mindful eating: lose the technology, maybe wean off through podcasts or paper books and magazines&lt;/li&gt;
&lt;li&gt;Mindful wake up and bed time: same deal, with emphasis not on the "perfect routine" but what works for me&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As for more visible goals, I'd like to end the year working a hybrid job in the city, living in a new apartment, having continued reading and writing and traveling and making new experiences for myself.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;This was one of the hardest years of my life, it's undeniable - as much as I want to deny it! I plan to reward the resilience that I've demonstrated with the time to process and heal. Even writing this post late (thanks stomach virus on new years eve!), and feeling the brunt of depression manifest, I feel my conscious contact being restored, and with it gratitude and hope for the new year.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>OR-Tools: Getting Help</title><link href="https://blog.tmk.name/2025/12/08/or-tools-getting-help/" rel="alternate"></link><published>2025-12-08T00:00:00-05:00</published><updated>2025-12-08T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-12-08:/2025/12/08/or-tools-getting-help/</id><summary type="html">&lt;p&gt;I've been programming with google's OR-Tools library for the past few years, working on solvers for the vehicle routing problem, scheduling problem, and set partitioning problem. As a developer without a background in optimization, the OR-Tools API is not intuitive, so I've had to pay my dues over the years …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've been programming with google's OR-Tools library for the past few years, working on solvers for the vehicle routing problem, scheduling problem, and set partitioning problem. As a developer without a background in optimization, the OR-Tools API is not intuitive, so I've had to pay my dues over the years crawling through documentation. It's been my experience that documentation is scattered; to that end, I thought it could be helpful to share the repositories of knowledge I've used, and how I use them. As of now (late 2025), llms are generally poor at generating code for OR-Tools, and given the complexity and subtlety of the software, it's good to verify generated code in any case.&lt;/p&gt;
&lt;h2&gt;Where to look&lt;/h2&gt;
&lt;p&gt;For general searches and problems, I'll start with the github and discord. Failing that, I'll search the google group archives. Google searches sometimes surface helpful stackoverflow posts. API changes over the years mean that old solutions sometimes need to be translated into new code structures.&lt;/p&gt;
&lt;p&gt;If I'm unable to make a dent in the problem, I'll ask in the discord - the OR-Tools developers and community are very friendly and have helped me several times. Don't expect an instant reply - it usually takes a day or so to hear back, though don't be surprised if you get a gist with a solution fully worked out.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/google/or-tools"&gt;github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.com/invite/ENkQrdf"&gt;discord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://groups.google.com/g/or-tools-discuss"&gt;google group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/optimization"&gt;core docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the vehicle routing problem specifically, the &lt;code&gt;routing.h&lt;/code&gt; file includes tons of helpful documentation, going beyond individual functions to strategies for solving problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/google/or-tools/blob/stable/ortools/constraint_solver/routing.h"&gt;routing.h&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'll also shout-out the plethora of examples in the repository:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/google/or-tools/blob/stable/ortools/constraint_solver/samples"&gt;samples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more in-depth reading, and some sample problem solutions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://activimetrics.com/blog/"&gt;activimetrics blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://acrogenesis.com/or-tools/documentation/user_manual/index.html"&gt;user manual&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="programming"></category></entry><entry><title>uvx flask</title><link href="https://blog.tmk.name/2025/11/24/uvx-flask/" rel="alternate"></link><published>2025-11-24T00:00:00-05:00</published><updated>2025-11-24T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-11-24:/2025/11/24/uvx-flask/</id><summary type="html">&lt;p&gt;This is a neat little trick that I couldn't find documented online. I am working on a small, single-file flask application and looking to keep my workflow as simple as possible. To that end, I am using &lt;code&gt;uvx&lt;/code&gt; (shorthand for &lt;code&gt;uv run&lt;/code&gt;) to &lt;a href="https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata"&gt;manage my dependencies&lt;/a&gt; in an implicit virtualenv …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is a neat little trick that I couldn't find documented online. I am working on a small, single-file flask application and looking to keep my workflow as simple as possible. To that end, I am using &lt;code&gt;uvx&lt;/code&gt; (shorthand for &lt;code&gt;uv run&lt;/code&gt;) to &lt;a href="https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata"&gt;manage my dependencies&lt;/a&gt; in an implicit virtualenv:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #7e8aa1"&gt;# /// script&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;# dependencies = [&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;#   &amp;quot;Flask&amp;quot;&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;# ]&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;# ///&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This new file format allows me to skip &lt;code&gt;requirements.txt&lt;/code&gt; and &lt;code&gt;pyproject.toml&lt;/code&gt;, which may not seem like much but keeps my entire software application in one file.&lt;/p&gt;
&lt;p&gt;To run the flask devserver, I added the following code:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;__name__&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;==&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;app&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;run(debug&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=True&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;port&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;PORT)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And run with &lt;code&gt;uvx app.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;While this is great for development, I wanted to use &lt;code&gt;gunicorn&lt;/code&gt; for production, which presented a challenge: gunicorn is its own command line tool and expects an wsgi application. Typically, running gunicorn for flask looks like:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;gunicorn&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--bind&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;0&lt;/span&gt;.0.0.0:PORT&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--workers&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;N&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;app:app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Naively, I tried:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;uvx&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;gunicorn&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--bind&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;0&lt;/span&gt;.0.0.0:PORT&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--workers&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;N&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;app:app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;but with this invocation, &lt;code&gt;uvx&lt;/code&gt; only installs &lt;code&gt;gunicorn&lt;/code&gt; into the virtualenv, and ignores dependencies in &lt;code&gt;app.py&lt;/code&gt;. Browsing the docs, I found &lt;code&gt;uvx --with-requirements &amp;lt;reqs...&amp;gt;&lt;/code&gt;, which supports &lt;code&gt;requirements.txt&lt;/code&gt; - but moving requirements out of the single file defeats the beauty of my single file application!&lt;/p&gt;
&lt;p&gt;On a hunch, I wondered if &lt;code&gt;uvx --with-requirements &amp;lt;reqs...&amp;gt;&lt;/code&gt; could read the requirements from the script itself (via the inline metadata), while running a separate tool, as in &lt;code&gt;uvx --with-requirements &amp;lt;script.py&amp;gt; &amp;lt;tool cli&amp;gt; ...&lt;/code&gt;. This worked! It's possible to define my requirements just once, and run a different tool with &lt;code&gt;uvx&lt;/code&gt;, leading to a true single file application experience:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;uvx&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--with-requirements&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;app.py&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;gunicorn&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--bind&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;0&lt;/span&gt;.0.0.0:PORT&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--workers&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;N&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;app:app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This approach also works with the flask cli, meaning that &lt;code&gt;app.run()&lt;/code&gt; can be removed. Tada 🎊&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;uvx&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--with-requirements&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;app.py&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;flask&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;run&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--port&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;PORT
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;tl;dr&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;uvx&lt;/code&gt; is a nifty tool for creating true single file applications (dependencies and all), extending to software served by other tools (e.g. flask web applications). The value of a single file application is subtle: easy to share with others, easy to ship, easy to work with an ai agent.&lt;/p&gt;
&lt;p&gt;To run a tool on a script with the script's dependencies:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;uvx&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--with-requirements&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&amp;lt;script.py&amp;gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&amp;lt;tool&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;cli&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="programming"></category></entry><entry><title>Parameterization</title><link href="https://blog.tmk.name/2025/11/10/parameterization/" rel="alternate"></link><published>2025-11-10T00:00:00-05:00</published><updated>2025-11-10T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-11-10:/2025/11/10/parameterization/</id><summary type="html">&lt;p&gt;Recently I was able to leverage parameterization to great effect, and I'd like to share the experience. I maintain an internal application that is run daily and produces some necessary logistics data for our operations. In extending the application, I was asked add support for running the tool for future …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Recently I was able to leverage parameterization to great effect, and I'd like to share the experience. I maintain an internal application that is run daily and produces some necessary logistics data for our operations. In extending the application, I was asked add support for running the tool for future dates.&lt;/p&gt;
&lt;h2&gt;Easy configuration&lt;/h2&gt;
&lt;p&gt;With several years of experience under my belt, I know that time is tricky to get right with software. At work, using Django, it's typical to rely on utilities like &lt;code&gt;timezone.now()&lt;/code&gt; and &lt;code&gt;timezone.localdate()&lt;/code&gt; to access the current time or date. As there's no restriction on calling these utilities, they may be used in deeply nested call trees. As the concept of a local date relies on global or implicit state (i.e. the active timezone), there's necessary implicit configuration required for the code to work correctly - and when the active timezone can be manipulated in the same fashion, down the call tree, it's easy to end up in a quagmire (yes, I've seen &lt;code&gt;timezone.activate(tz)&lt;/code&gt; used in a loop, leading to a random timezone ultimately activated at the end).&lt;/p&gt;
&lt;p&gt;So when I was asked update my application, how much did I suffer in chasing down time related bugs? None (mostly). And how is that, you wonder? Did I leverage more global implicit behavior like overriding the computer time? Nope. I had, long ago, already parameterized the application on date.&lt;/p&gt;
&lt;p&gt;By parameterizing, I mean adding explicit function parameters. E.g.&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;get_orders_for_today&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(today):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;Order&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;objects&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;filter(date&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;today)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;get_orders_for_today&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;():&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;Order&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;objects&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;filter(date&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;timezone&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;localdate())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In my application code, the top level function (interface) accepted a date parameter and passed it down the call tree. The date had been previously passed to the function with &lt;code&gt;timezone.localdate()&lt;/code&gt; - I swapped this for reading the date from user input. I verified that there was no implicit behavior based on the date by following its usage. And I was able to get this feature done in an afternoon instead of a week.&lt;/p&gt;
&lt;h2&gt;Easy testing&lt;/h2&gt;
&lt;p&gt;Testing also benefits from parameterization, as an alternative to monkeypatching. As an example, at work, we have an optimization problem solver which calls out to a native library and typically runs for several seconds. To test this code, the relevant &lt;code&gt;solver()&lt;/code&gt; function could be monkeypatched to skip the processing and return some value. The alternative approach that I used is called the &lt;a href="https://en.wikipedia.org/wiki/Strategy_pattern"&gt;strategy pattern&lt;/a&gt;, where the algorithm is passed in as a parameter. Suppose I have the following interface:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;ISolver&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;solve&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(problem):&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, I can implement the native solver &lt;em&gt;and&lt;/em&gt; a stub solver:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;NativeSolver&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(ISolver):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;solve&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(problem):&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;time_consuming_solver(problem)&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;StubSolver&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(ISolver):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;__init__&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;solution):&lt;/span&gt;
        &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;solution&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;solution&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;solve&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(problem):&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;solution&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;Yes, this could be improved with typing and protocols, but that's besides the point here&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally, I'd parameterize my application code:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;scheduling_problem&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(problem,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;solver):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt; &lt;span style="color: #7e8aa1"&gt;# call solver(problem)&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;solver&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;NativeSolver()&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;scheduling_problem(problem,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;solver)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And to test:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;def test_scheduling_problem():
    problem = ...
    solution = ...
    solver = StubSolver(solution)
    scheduling_problem(problem)
    assert ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Admittedly, I've had less success spreading the gospel on the benefits of the strategy pattern, the core critique being the boilerplate code. That's fair. I have only used this technique a handful of times, most commonly when there are multiple strategy implementations besides the test mock.&lt;/p&gt;
&lt;h2&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;I recognize this technique isn't revolutionary, as it's really just a simple application of programming principles: don't rely on or mutate implicit or global state. As with most timeless advice, it's often ignored and quite beneficial.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Scratch the Itch</title><link href="https://blog.tmk.name/2025/10/23/scratch-the-itch/" rel="alternate"></link><published>2025-10-23T00:00:00-04:00</published><updated>2025-10-23T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-10-23:/2025/10/23/scratch-the-itch/</id><summary type="html">&lt;p&gt;Today I had the opportunity to practice a favorite technique: &lt;em&gt;scratching the itch&lt;/em&gt;. By this I mean I let my curiosity take charge, quieting the know-it-all that typically runs the show.&lt;/p&gt;
&lt;h2&gt;The itch&lt;/h2&gt;
&lt;p&gt;I was working on a section of frontend typescript code. I had noticed a few days before …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Today I had the opportunity to practice a favorite technique: &lt;em&gt;scratching the itch&lt;/em&gt;. By this I mean I let my curiosity take charge, quieting the know-it-all that typically runs the show.&lt;/p&gt;
&lt;h2&gt;The itch&lt;/h2&gt;
&lt;p&gt;I was working on a section of frontend typescript code. I had noticed a few days before that my editor was complaining that a type was undefined. That is, for&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;TypeScript&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;let&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; x&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #73D0FF"&gt;Foo&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;my editor put squigglies under &lt;code&gt;Foo&lt;/code&gt; and complained. This isn't unusual, as I am used to typescript errors appearing temporarily when switching between branches: the in-memory diagnostics haven't caught up to the filesystem changes yet. Besides, this code was already merged into develop, with CI checks passed. It must be my editor.&lt;/p&gt;
&lt;p&gt;It must be, right? I suddenly felt the &lt;em&gt;itch&lt;/em&gt;. The itch is molded with experience, but experience isn't required: the itch may better be framed as childlike curiosity. During service at my Buddhist monastery, I realized with delight that children in the audience asked questions that I was too ashamed to ask. And how does that shame serve me, when it holds me back? Adopting a position of innocence and curiosity can be a rewarding endeavor.&lt;/p&gt;
&lt;p&gt;To scratch this particular itch, I actually looked at the error message: cannot find name &lt;code&gt;Foo&lt;/code&gt;. I won't be too proud to admit that I sometimes glance over error messages, jumping to conclusions with a presumed root cause. Here was my (abbreviated) thought process:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This error message was strange, how could Foo not be found? Let me check the editor: reload the file. Error still present. Okay, is Foo not defined? Indeed, it is not! How does that make sense, if type checking in CI is passing? Let me run type checking outside the editor, with the CI command. Oh that's odd - the type checking command instantly succeeds. I'd expect it to take a few seconds - it's a hint when a tool succeeds too quickly. Let me check the typescript config.. hmm, I'm not familiar with this config approach, but I recall that we recently migrated from webpack to vite - let me ask gpt quickly. Aha - gpt suggests that running tsc with this tsconfig will not perform project type checking. That explains why typescript errors are present on develop - CI was passing on broken code! Ah, with the build flag, tsc should work.. yes, I now see the typescript errors!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This took about ten minutes. There are a dozen engineers on the team, and the misconfigured typescript check had been present for three months. It's not a dig on the team that it wasn't found until now - it's a signal of how much differentiation this small technique can provide for an engineer.&lt;/p&gt;
&lt;h2&gt;Lessons&lt;/h2&gt;
&lt;p&gt;The story is representative of years of applying this technique. I'll summarize a few takeaways/lessons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;When something seems off, investigate.&lt;/strong&gt; I ignored the error for several days before taking a look.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Double check assumptions.&lt;/strong&gt; I assumed that the problem was local, because CI &lt;em&gt;must be passing&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time box.&lt;/strong&gt; I stopped at identifying the broken type check command - surfacing the issue is valuable, and my teammate who configured the new system will be better suited to fix it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of the time, these investigations don't pay big dividends: it's usually my implicit assumptions that are proved wrong in the end. That's fine, the active learning process itself is rewarding, as I hone my understanding of the system in addition to my investigation skills. Sometimes, though, the payoffs are big. As an example, I once found a &lt;a href="https://blog.tmk.name/2025/03/02/security-stories-part-1/"&gt;password bypass in production&lt;/a&gt; when I scratched an itch.&lt;/p&gt;</content><category term="career"></category></entry><entry><title>Visualizing my Wikipedia Browsing History</title><link href="https://blog.tmk.name/2025/10/13/visualizing-my-wikipedia-browsing-history/" rel="alternate"></link><published>2025-10-13T00:00:00-04:00</published><updated>2025-10-13T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-10-13:/2025/10/13/visualizing-my-wikipedia-browsing-history/</id><summary type="html">&lt;p&gt;&lt;img alt="full" src="https://blog.tmk.name/images/wikitrack/full.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Full network graph. Ring is pages not connected. The dense inner web demonstrates how highly connected wikipedia pages are&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="scifi" src="https://blog.tmk.name/images/wikitrack/scifi.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Zoom in on my favorite author: Iain Banks and the Culture series&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="greece" src="https://blog.tmk.name/images/wikitrack/greece.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cluster of ancient Greek philosophy and history&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Backstory&lt;/h2&gt;
&lt;p&gt;I have been intentionally changing my internet browsing history the past …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="full" src="https://blog.tmk.name/images/wikitrack/full.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Full network graph. Ring is pages not connected. The dense inner web demonstrates how highly connected wikipedia pages are&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="scifi" src="https://blog.tmk.name/images/wikitrack/scifi.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Zoom in on my favorite author: Iain Banks and the Culture series&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="greece" src="https://blog.tmk.name/images/wikitrack/greece.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cluster of ancient Greek philosophy and history&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Backstory&lt;/h2&gt;
&lt;p&gt;I have been intentionally changing my internet browsing history the past few years, aiming to reduce endless scrolling on "junk food" sites. This year in particular, I quit reddit and twitter, and have instead been reading just the news and wikipedia. This has worked as a gradual reduction in satisfaction: the news, while interesting, lacks the same level of excitement as a silly debate on reddit or "hot take" from a personality on twitter. I regularly browse the Wikipedia "Did you know..." and "This day in history," finding it scratches the itch of new content, while again lacking the compulsive pull that lost me countless hours previously.&lt;/p&gt;
&lt;p&gt;As a result, I have visited about 10,000 wikipedia articles. As a creative programmer, I was inspired to dive into this data set - could I visualize the connections between pages, for example?&lt;/p&gt;
&lt;h2&gt;Approach&lt;/h2&gt;
&lt;p&gt;Two books played a large role in this project coming to fruition. &lt;em&gt;Nexus&lt;/em&gt; by Mark Buchanan explores network theory, explaining various network topologies and applications to the real world - hence my interest in modeling my browsing history as a network. On the creative side, I recently started &lt;em&gt;The Artist's Way&lt;/em&gt; by Julia Cameron, with the intention of recovering the artist within me. One key challenge for me is perfectionism: I bring my professional background, but more than that, I bring high standards to what are casual and fun projects.&lt;/p&gt;
&lt;p&gt;I think that llms can be a boon for creative blocks and perfectionism. I started a chat with a prompt that explained my default tendencies, such as over-engineering, getting lost in details, and struggling to finish and publish. I then worked with intention, describing my actions step by step, and getting helpful feedback and perspective - the llm caught a few moments where I almost went down a rabbit hole. Most notably, the llm suggested I call the project &lt;em&gt;done&lt;/em&gt; and write this article. I believe that llms can be utilized to great benefit for people skilled at introspection: knowing my blindspots and tendencies, and communicating with honesty and accepting the feedback, I'm able to break unskillful patterns of behavior.&lt;/p&gt;
&lt;h2&gt;Technical details&lt;/h2&gt;
&lt;p&gt;The high level technical approach was as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get my wikipedia browsing history&lt;/li&gt;
&lt;li&gt;Build a network data structure where articles are nodes and links between articles are edges&lt;/li&gt;
&lt;li&gt;Visualize the network&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Browsing history&lt;/h3&gt;
&lt;p&gt;I don't have an account on wikipedia, but I don't believe that browsing history is tracked with an account anyway. I do use firefox with sync across desktop and mobile, which conveniently syncs browsing history - important because I visit wikipedia on multiple devices. I therefore built a browser extension, which enabled retrieving history, and a convenient interface for visualizing in the browser via extension pages.&lt;/p&gt;
&lt;p&gt;The extension uses a browser action to open an extension page - i.e. the toolbar extension button opens a new tab with content from the extension. The technical details for this approach are scattered across the mdn docs and examples, here's the basics.&lt;/p&gt;
&lt;p&gt;Here's my &lt;code&gt;manifest.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;JSON&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;{&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;&amp;quot;manifest_version&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;: &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;2&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;: &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Wikitrack&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;&amp;quot;version&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;: &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;0.1&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;: &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Visualize wikipedia browsing history.&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;&amp;quot;browser_action&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;: {&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    },&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;&amp;quot;background&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;: {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        &lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;&amp;quot;scripts&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;: [&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;background-script.js&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    },&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;&amp;quot;permissions&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;: [&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;history&amp;quot;&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    ]&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The relevant bits are defining the &lt;code&gt;browser_action&lt;/code&gt; entry (it can be empty for a simple extension) and the background scripts. Here's &lt;code&gt;background-script.js&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;JavaScript&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;function&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; listener () {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    browser.tabs.create({&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        url&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;page.html&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    });&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;}&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;browser.browserAction.onClicked.addListener(listener)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This defines a listener on the browser action, opening my &lt;code&gt;page.html&lt;/code&gt; when the toolbar button is clicked. Great. The html is basic and references a javascript file that grabs the history (because extension pages have access to the same permissions as the extension):&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;JavaScript&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;function&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; onGot(historyItems) {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    console.log(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Found &amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;+&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; historyItems.length &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;+&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot; wikipedia pages visited&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;);&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;}&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;browser.history&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    .search({&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        text&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;wikipedia.org&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        startTime&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;0&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        maxResults&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;1000000&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    })&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    .then(onGot);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Awesome! I am able to get a list of all wikipedia pages that I've visited.&lt;/p&gt;
&lt;h3&gt;Network&lt;/h3&gt;
&lt;p&gt;With the browsing history, the next step was to build a network data structure that included links between pages as edges. To get the links, I needed to get each article's content. There were a few approaches that I could've taken, the simplest most likely scraping the live wikipedia site to retrieve each article's content. At the scale of 10k articles, mindful of rate limits, and maybe a little overengineering, I decided to take the approach of parsing the full english wikipedia data dump instead.&lt;/p&gt;
&lt;p&gt;This came with some challenges: as the data dump is 100gb decompressed, the suggested method is to use an index into the compressed file, and decompress only what's necessary. This seemed technically challenging and therefore a potential rabbit hole, so I chose to decompress the file and stream parse the xml. I tried various tools, including xmlstarlet, xmllint, and python's builtin xml parser, before settling on lxml: with stream parsing, lxml processed up to 50k articles per second. At roughly 25 million articles total, processing the entire 100gb xml file took about 9 minutes. This was perfectly reasonable for my use case: find articles that I've visited and output the article content.&lt;/p&gt;
&lt;p&gt;Here's the script that stream processes the xml, writing a jsonl file with article contents:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;json&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;lxml&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;etree&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;tqdm&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;tqdm&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;page_estimate_count&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #DFBFFF"&gt;25_040_290&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;visited_page_titles&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFD173"&gt;set&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;()&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;pages_found_in_xml&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFD173"&gt;set&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;()&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;pages_found_in_xml_count&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #DFBFFF"&gt;0&lt;/span&gt;


&lt;span style="color: #FFAD66"&gt;with&lt;/span&gt; &lt;span style="color: #FFD173"&gt;open&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;history.cleaned.json&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;r&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;as&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;history_file:&lt;/span&gt;
    &lt;span style="color: #7e8aa1"&gt;## history.cleaned.json is a json list of article titles, cleaned and filtered (e.g. special pages removed)&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;history_content&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;json&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;load(history_file)&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;visited_page_titles&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFD173"&gt;set&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(history_content)&lt;/span&gt;

&lt;span style="color: #FFD173"&gt;print&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #F29E74"&gt;f&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Looking for &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;{&lt;/span&gt;&lt;span style="color: #FFD173"&gt;len&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(visited_page_titles)&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;}&lt;/span&gt;&lt;span style="color: #D5FF80"&gt; pages&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;with&lt;/span&gt; &lt;span style="color: #FFD173"&gt;open&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;
    &lt;span style="color: #D5FF80"&gt;&amp;quot;wikidata/enwiki-20251001-pages-articles-multistream.xml&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;rb&amp;quot;&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;as&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;xml_file,&lt;/span&gt; &lt;span style="color: #FFD173"&gt;open&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;history.pagecontent.jsonl&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;w&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;as&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;pagecontent_file,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;tqdm(&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;total&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;page_estimate_count,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;desc&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Processing pages&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;unit&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;pages&amp;quot;&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;as&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;pbar:&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;event,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;elem&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;etree&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;iterparse(xml_file,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;events&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;end&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,)):&lt;/span&gt;

        &lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;elem&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;tag&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;endswith(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;page&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;):&lt;/span&gt;

            &lt;span style="color: #7e8aa1"&gt;# get title + text&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;title_el&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;elem&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;find(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;.//{http://www.mediawiki.org/xml/export-0.11/}title&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

            &lt;span style="color: #d4d2c8"&gt;text_el&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;elem&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;find(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;.//{http://www.mediawiki.org/xml/export-0.11/}text&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

            &lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;title_el&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;is&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;not&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;None&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;and&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;text_el&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;is&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;not&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;None&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
                &lt;span style="color: #d4d2c8"&gt;page_title&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;title_el&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;text&lt;/span&gt;
                &lt;span style="color: #d4d2c8"&gt;page_content&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;text_el&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;text&lt;/span&gt;

                &lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;page_title&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;visited_page_titles:&lt;/span&gt;
                    &lt;span style="color: #d4d2c8"&gt;pages_found_in_xml&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;add(page_title)&lt;/span&gt;
                    &lt;span style="color: #d4d2c8"&gt;pages_found_in_xml_count&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;+=&lt;/span&gt; &lt;span style="color: #DFBFFF"&gt;1&lt;/span&gt;

                    &lt;span style="color: #d4d2c8"&gt;pbar&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;set_postfix(found&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;pages_found_in_xml_count)&lt;/span&gt;

                    &lt;span style="color: #d4d2c8"&gt;pagecontent_file&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;write(&lt;/span&gt;
                        &lt;span style="color: #d4d2c8"&gt;json&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;dumps(&lt;/span&gt;
                            &lt;span style="color: #d4d2c8"&gt;{&lt;/span&gt;
                                &lt;span style="color: #D5FF80"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;page_title,&lt;/span&gt;
                                &lt;span style="color: #D5FF80"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;page_content,&lt;/span&gt;
                            &lt;span style="color: #d4d2c8"&gt;}&lt;/span&gt;
                        &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
                        &lt;span style="color: #FFAD66"&gt;+&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;\n&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;&lt;/span&gt;
                    &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

            &lt;span style="color: #d4d2c8"&gt;pbar&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;update(&lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;1&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

            &lt;span style="color: #7e8aa1"&gt;# Free memory as you go&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;title_el&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;text_el&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;None&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;elem&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;clear()&lt;/span&gt;

            &lt;span style="color: #7e8aa1"&gt;# Remove references from parent to enable garbage collection&lt;/span&gt;
            &lt;span style="color: #FFAD66"&gt;while&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;elem&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;getprevious()&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;is&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;not&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;None&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
                &lt;span style="color: #FFAD66"&gt;del&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;elem&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;getparent()[&lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;0&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]&lt;/span&gt;

&lt;span style="color: #FFD173"&gt;print&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #F29E74"&gt;f&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Found &lt;/span&gt;&lt;span style="color: #95E6CB"&gt;{&lt;/span&gt;&lt;span style="color: #FFD173"&gt;len&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(pages_found_in_xml)&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;}&lt;/span&gt;&lt;span style="color: #D5FF80"&gt; pages from history in the xml&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;span style="color: #FFD173"&gt;print&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Of note, stream processing via &lt;code&gt;.iterparse()&lt;/code&gt; does not free memory automatically. Instead, memory for each element has to be explicitly freed via &lt;code&gt;elem.clear()&lt;/code&gt; - however this only frees its &lt;em&gt;children&lt;/em&gt;, not itself, so the references to the element from the parent also have to be explicitly removed. All together, this script uses 40-80mb of memory, quite impressive for processing a 100gb file!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tqdm&lt;/code&gt;, with the ability to specify a total and custom postfix content, provided an excellent interface:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;%&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;python&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;graph.py
Looking&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;for&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;9051&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;pages
Processing&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;pages:&lt;span style="color: #d4d2c8"&gt;   &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;3&lt;/span&gt;%&lt;span style="color: #d4d2c8"&gt;|&lt;/span&gt;██▋&lt;span style="color: #d4d2c8"&gt;                         | &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;689010&lt;/span&gt;/25040290&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;[&lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;00&lt;/span&gt;:26&amp;lt;&lt;span style="color: #DFBFFF"&gt;11&lt;/span&gt;:13,&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;36150&lt;/span&gt;.31pages/s,&lt;span style="color: #d4d2c8"&gt; found&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;3553&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The next script parsed each article's content using &lt;code&gt;mwparserfromhell&lt;/code&gt;, extracting links, and built a simple network data structure, mapping each article to its linked articles (limited to articles I've visited, i.e. no new nodes are introduced):&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;json&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;collections&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;defaultdict&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;mwparserfromhell&lt;/span&gt;


&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;is_valid_link&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(link):&lt;/span&gt;
    &lt;span style="color: #7e8aa1"&gt;# Filter out images, categories, and non-internal links&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;not&lt;/span&gt; &lt;span style="color: #FFD173"&gt;str&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(link&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;title)&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;startswith(&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;File:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;Category:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;Talk:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;User:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;Template:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;Help:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;Portal:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;Draft:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;TimedText:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;MediaWiki:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;Module:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
            &lt;span style="color: #D5FF80"&gt;&amp;quot;Special:&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;


&lt;span style="color: #d4d2c8"&gt;pagecontent&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;[]&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;with&lt;/span&gt; &lt;span style="color: #FFD173"&gt;open&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;history.pagecontent.jsonl&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;as&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;pagecontent_file:&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;line&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;pagecontent_file:&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;pagecontent&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;append(json&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;loads(line&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;strip()))&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;full_graph&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;defaultdict(&lt;/span&gt;&lt;span style="color: #FFD173"&gt;set&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;page&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;pagecontent:&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;wikicode&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;mwparserfromhell&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;parse(page[&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;])&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;links&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;[]&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;node&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;wikicode&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;filter_wikilinks():&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;is_valid_link(node):&lt;/span&gt;
            &lt;span style="color: #d4d2c8"&gt;links&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;append(node)&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;full_graph[page[&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]]&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;update(&lt;/span&gt;&lt;span style="color: #FFD173"&gt;str&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(link&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;title)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;link&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;links)&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;interested_pages&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFD173"&gt;set&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(full_graph&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;keys())&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;interested_graph&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;defaultdict(&lt;/span&gt;&lt;span style="color: #FFD173"&gt;list&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;for&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;interested_page&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;in&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;interested_pages:&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;edges&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;full_graph[interested_page]&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;interested_edges&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;edges&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;intersection(interested_pages)&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;interested_graph[interested_page]&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFD173"&gt;list&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(interested_edges)&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;with&lt;/span&gt; &lt;span style="color: #FFD173"&gt;open&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;history.graph.json&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;w&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;as&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;graph_file:&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;json&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;dump(interested_graph,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;graph_file)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;After about a minute, I have my graph data structure, ready to visualize!&lt;/p&gt;
&lt;h3&gt;Visualization&lt;/h3&gt;
&lt;p&gt;For visualization of a network in the browser, I first tried vis.js, but it choked under the load of 10k nodes and 20k edges, so I went with Sigma.js, which is better suited for this network size. Keeping it simple - no webpack or bundling - I downloaded the graphology and sigma js and loaded them directly into the html. It's amazing how fast development can be without all the cruft of modern tooling!&lt;/p&gt;
&lt;p&gt;For loading the data, again I took a simple approach and manually edited &lt;code&gt;history.graph.json&lt;/code&gt; to a javascript file that exports the json, and loaded the javascript file into the html. The code to render the graph is thus:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;JavaScript&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;$(&lt;/span&gt;&lt;span style="color: #FFD173"&gt;document&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;).ready(&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;function&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; () {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;const&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; container &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFD173"&gt;document&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;.getElementById(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;network&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;);&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;// graphData is exported implicitly via js file&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;// sigma.js&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;const&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; graph &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;new&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; graphology.Graph();&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;// Add nodes&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #FFD173"&gt;Object&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;.keys(graphData).forEach(node =&amp;gt; {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        graph.addNode(node, { label&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; node });&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    });&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;// Add edges&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #FFD173"&gt;Object&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;.keys(graphData).forEach(source =&amp;gt; {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        graphData[source].forEach(target =&amp;gt; {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;            graph.addEdge(source, target);&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;        });&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    });&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;// Position nodes on a circle, then run Force Atlas 2 for a while to get proper graph layout:&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    graphologyLibrary.layout.circular.assign(graph);&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;const&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; settings &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; graphologyLibrary.layoutForceAtlas2.inferSettings(graph);&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    graphologyLibrary.layoutForceAtlas2.assign(graph, { settings, iterations&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;600&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; });&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;const&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; renderer &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;new&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; Sigma(graph, container);&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Voila! From 0 to a visualization in a weekend.&lt;/p&gt;
&lt;h2&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;I actually did rediscover some of the joy that I used to have with programming, way back in college when I "didn't know better": poorly written scripts that glue together through tedious manual processes, pulling in dependencies in the simplest way possible (pip install and downloading the js), settling for "good enough" hacks that get the job done. "Done is better than perfect" comes to mind, as I can publish this article today instead of never.&lt;/p&gt;
&lt;p&gt;Some follow-up projects that come to mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Improved visualization and exploration of visualization techniques&lt;/li&gt;
&lt;li&gt;Professionalize the project, bundle and ship it for others as an extension and web api&lt;/li&gt;
&lt;li&gt;Building an interactive network UI, with filtering and timeline support&lt;/li&gt;
&lt;li&gt;Clustering and labeling&lt;/li&gt;
&lt;li&gt;"Missed connections": suggest articles that are related to articles I've visited&lt;/li&gt;
&lt;li&gt;Sessions: visualize individual browsing sessions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More to come?&lt;/p&gt;
&lt;h2&gt;Data sources&lt;/h2&gt;
&lt;p&gt;This project uses data from Wikipedia, available under the &lt;a href="https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License"&gt;Creative Commons Attribution-ShareAlike License&lt;/a&gt;. The English Wikipedia database dump was obtained from &lt;a href="https://en.wikipedia.org/wiki/Wikipedia:Database_download"&gt;Wikimedia Downloads&lt;/a&gt;.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Layers</title><link href="https://blog.tmk.name/2025/10/10/layers/" rel="alternate"></link><published>2025-10-10T00:00:00-04:00</published><updated>2025-10-10T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-10-10:/2025/10/10/layers/</id><summary type="html">&lt;p&gt;I'm back from my trip to Greece. Three weeks of travel feels like a year; in measurements: 4 cities, 5 beds, 3 flights, 2 ferries, 1 car rental, 5 beaches, 5 museums. Greece felt to me like a land of contradictions, rich and poor, fast and slow, punctuality and delays …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm back from my trip to Greece. Three weeks of travel feels like a year; in measurements: 4 cities, 5 beds, 3 flights, 2 ferries, 1 car rental, 5 beaches, 5 museums. Greece felt to me like a land of contradictions, rich and poor, fast and slow, punctuality and delays, bustling and quiet. My experience was much the same, contradictory in many of the same ways. It all blended together to give me time, space and a mirror for self reflection.&lt;/p&gt;
&lt;h2&gt;History&lt;/h2&gt;
&lt;p&gt;&lt;img alt="bull leaping fresco" src="https://blog.tmk.name/images/fresco.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I was dismayed to learn that the frescoes were re-imagined from fragments - yet how different is this from recollection and interpretation of my own fragmentary memories?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Browsing the galleries of the archaeological museum in Heraklion it suddenly became obvious to me the parallels between human history and my own. For my entire life I've other-ized myself, put myself apart from people, constructed an internal framework where I am special and unique. My ego ran the show, judging and measuring, comparing and differentiating itself. My ego believes itself to be unlimited in its potential, and views others as tools to be used to achieve a greatness that would reflect its desired self image back at it: that of power, and control.&lt;/p&gt;
&lt;p&gt;In the history of the earliest European civilization, I was ready to be humbled. Just as the course of a civilization is determined by causes and conditions inside and outside of its control, so is my own course of life. Just as thousands, millions and even billions of people live their lives determined by causes and conditions inside and outside of their control, so do I. A part of me that was holding onto my ego was able to slacken, and I feel the pressure decreased.&lt;/p&gt;
&lt;h2&gt;Origins&lt;/h2&gt;
&lt;p&gt;For decades I thought I understood my childhood. I was able to accept that it was out of the ordinary, if only based on the amount of time it took to explain the rotating cast of characters and scene transitions. A gift of insight that I've received is a new perspective, one which has been the key to a transformation of my inner and outer worlds.&lt;/p&gt;
&lt;p&gt;At a very young age, I lost my family. I don't know exactly when or how, partly because of my age at the time, and partly because the participants are unable to speak of it, whether through death or denial. I thought I understood this event in my childhood, despite my ignorance, but as time goes on, I am coming to see how deeply this affected me. It seems simplistic to trace the course of my life through one event, a sort of fatalism - I don't believe the future had been fixed at that point, anymore than it is right now - but I do believe that some events or processes dominate the trajectory of the future.&lt;/p&gt;
&lt;p&gt;In response to this overwhelming tragedy, I can appreciate how my ego grew outsized, in an attempt to protect a vulnerable child. I can appreciate how my ego became reinforced with the notion that other people are dangerous and cannot be trusted. I can appreciate how I came to see myself as fundamentally different than other human beings.&lt;/p&gt;
&lt;h2&gt;Change&lt;/h2&gt;
&lt;p&gt;I struggle to feel calm, safe and stable in my life. Even simple acts like sitting in the park spiral into future or guilt tripping, anxiety taking over. As I've come to learn, this is an inability for my nervous system to relax, put another way for myself to turn off for a while. I can intellectually understand that I am in a completely safe situation, yet my body does not feel it.&lt;/p&gt;
&lt;p&gt;Form and content: my intellectual framework and emotional reality grow and change at different rates in different times. An intellectual understanding of my childhood does not immediately yield an emotional transformation. On the other hand, consistent effort towards healing emotional wounds is not immediately noticed: travels are a reset that enable me to see my growth.&lt;/p&gt;
&lt;p&gt;For my entire life the effort to heal has been a complete and total mystery. I was raised with unhealthy expectations around learning, I was expected to "get it" on the first try, by myself - to ask for help risked wrath instead of vulnerability. Taking this into my adulthood, I tried and failed countless times, using the result as proof of my deficiencies. I'm now learning how to learn, as I'm shedding these old mental habits and gaining emotional resilience. &lt;/p&gt;
&lt;p&gt;I am realizing that repetition and initial failures are part of the learning process. Approaching new activities or topics with calm and curiosity serve me better than anxiety and insecurity. Learning with others can be a source of inspiration and energy, not just judgment.&lt;/p&gt;
&lt;h2&gt;Layers&lt;/h2&gt;
&lt;p&gt;Moreover, I am realizing that mindful repetition is key to healing the deep, emotional parts. A Buddhist monk shared about this recently, saying that it's now well understood how sensations build into feelings into perceptions and finally into conscious thoughts. She followed up that the opposite may be practiced: from thought down to feeling and sensation. This is loving kindness, mindfulness, equanimity, reframing, etc. - applying the mind to influence the heart.&lt;/p&gt;
&lt;p&gt;The key revelation for me is the repetition. I am so used to expecting of myself instant results. Faith is the belief that change and growth are possible, even when I can't comprehend it. As another monk shared once, faith can be as simple as believing that someone is further along the path than I am, that if they can do it, so can I.&lt;/p&gt;
&lt;p&gt;The training of the mind and heart takes time, effort, and repetition. For matters of the heart in particular, there's no guarantees on timeline. The Buddhists like to say that it takes decades of consistent practice to achieve any meaningful progress towards enlightenment. I think the point about the duration is not to discourage the practice, but the ego. I am coming to see, in glimmers of insight, that the practice itself is the enlightenment, both the goal and the destination. That it takes a lifetime to achieve - well, it's a lifetime of practice that I can learn to appreciate and enjoy.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Investigating the Pytest Django Teardown Race Condition</title><link href="https://blog.tmk.name/2025/09/17/investigating-the-pytest-django-teardown-race-condition/" rel="alternate"></link><published>2025-09-17T00:00:00-04:00</published><updated>2025-09-17T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-09-17:/2025/09/17/investigating-the-pytest-django-teardown-race-condition/</id><summary type="html">&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;In my &lt;a href="https://blog.tmk.name/2025/04/06/pytest-playwright-and-django/"&gt;previous post&lt;/a&gt; about pytest, playwright and django, I described an annoying non deterministic failure on test teardown. I believe the issue is a race condition that occurs when the test database is flushed, while network requests are pending. The &lt;a href="https://github.com/pytest-dev/pytest-django/issues/454"&gt;reference issue&lt;/a&gt; agrees with the theory but does …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;In my &lt;a href="https://blog.tmk.name/2025/04/06/pytest-playwright-and-django/"&gt;previous post&lt;/a&gt; about pytest, playwright and django, I described an annoying non deterministic failure on test teardown. I believe the issue is a race condition that occurs when the test database is flushed, while network requests are pending. The &lt;a href="https://github.com/pytest-dev/pytest-django/issues/454"&gt;reference issue&lt;/a&gt; agrees with the theory but does not detail a root cause, nor are the solutions insightful or practical.&lt;/p&gt;
&lt;p&gt;I let this issue simmer; the band-aid of explicitly waiting for network requests to finish at the end of each test mostly worked. It was far from satisfactory, however, being a guard that developers would have to mindfully add to every test. Over time, changes in network requests also meant failing tests. In short, my team reached the tipping point at which a more thorough solution was necessary to continue investing in playwright tests.&lt;/p&gt;
&lt;h2&gt;Investigation&lt;/h2&gt;
&lt;p&gt;In past investigations, I placed a degree of trust in the library code. After all, doubting the code that I depend on can lead to some significant trust issues. This time around though, I was determined to answer the question that bugged me: why are there unfinished requests at test teardown? To find the answer, I had to look into the source for pytest-django, django, and the python standard library.&lt;/p&gt;
&lt;p&gt;pytest-django provides the &lt;code&gt;live_server&lt;/code&gt; fixture, which exposes an http server backed by the django application on a local port. This fixture delegates to the &lt;code&gt;LiveServer&lt;/code&gt; class, which mirrors the &lt;code&gt;LiveServerTestCase&lt;/code&gt; class provided by django, re-implemented to work as a pytest fixture. The &lt;code&gt;LiveServer&lt;/code&gt; class starts a &lt;code&gt;LiveServerThread&lt;/code&gt; (also provided by django), which is responsible for actually running the http server.&lt;/p&gt;
&lt;p&gt;Ok, still with me? The &lt;code&gt;LiveServerThread&lt;/code&gt; is terminated at the end of the &lt;code&gt;live_server&lt;/code&gt; fixture. In theory, then, pending requests should be forcefully terminated. Ah, but that would only be the case if &lt;code&gt;LiveServerThread&lt;/code&gt; was responsible for processing requests itself - in fact, it isn't. The default &lt;code&gt;server_class&lt;/code&gt; is django's &lt;code&gt;ThreadedWSGIServer&lt;/code&gt;, which processes each incoming request in its own thread - meaning that the server is capable of concurrent request processing. The server class is powered by the python standard library, leveraging &lt;code&gt;socketserver.ThreadingMixIn&lt;/code&gt; for the per-thread behavior.&lt;/p&gt;
&lt;p&gt;Notably, &lt;code&gt;ThreadedWSGIServer&lt;/code&gt; configures the request handling threads to be daemons, per the docs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When inheriting from ThreadingMixIn for threaded connection behavior, you should explicitly declare how you want your threads to behave on an abrupt shutdown. The ThreadingMixIn class defines an attribute daemon_threads, which indicates whether or not the server should wait for thread termination. You should set the flag explicitly if you would like threads to behave autonomously; the default is False, meaning that Python will not exit until all threads created by ThreadingMixIn have exited.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This means that, while the &lt;code&gt;LiveServerThread&lt;/code&gt; will exit cleanly, any pending request threads will not be joined / waited upon to finish. Mystery solved? If any of those requests are interacting with the database, there's the possibility for a deadlock between the request thread and the main thread attempting to flush the test database.&lt;/p&gt;
&lt;h2&gt;A possible solution&lt;/h2&gt;
&lt;p&gt;A relatively straightforward, if not pretty, solution is to override the configuration of &lt;code&gt;ThreadedWSGIServer&lt;/code&gt; in a &lt;code&gt;live_server&lt;/code&gt; fixture override:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django.core.servers.basehttp&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;ThreadedWSGIServer&lt;/span&gt;

&lt;span style="color: #7e8aa1; font-weight: bold; font-style: italic"&gt;@pytest&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;fixture&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;live_server&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(live_server):&lt;/span&gt;
    &lt;span style="color: #7e8aa1"&gt;# wait on requests to finish before shutting down the server&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;ThreadedWSGIServer&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;daemon_threads&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;False&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;yield&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;live_server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This change will result in the server waiting for request thread completion before itself shutting down. As it's isolated to the test suite, it's not the most abominable monkeypatch imaginable, but I'd prefer a cleaner solution whereby pytest-django would support customization of the underlying &lt;code&gt;LiveServerThread&lt;/code&gt;, to modify the &lt;code&gt;server_class&lt;/code&gt; (I believe this is better supported by django's unit test framework).&lt;/p&gt;
&lt;h2&gt;Unfinished business&lt;/h2&gt;
&lt;p&gt;This investigation cleanly answers my original question: why are there unfinished network requests at test teardown? The requests are handled in separate, daemon threads that are not stopped / waited on when the &lt;code&gt;live_server&lt;/code&gt; fixture cleans up.&lt;/p&gt;
&lt;p&gt;In the pytest-django source though, I noticed another behavior: the &lt;code&gt;live_server&lt;/code&gt; fixture is session scoped by default. This means the fixture provides the same &lt;code&gt;LiveServer&lt;/code&gt; instance for every test in a given run. Therefore the server class won't stop until all tests have run, or put another way, those request threads won't be joined at the end of each test, so there's still the possibility for the race condition.&lt;/p&gt;
&lt;p&gt;To solve this, my untested &lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; theory is to make the &lt;code&gt;live_server&lt;/code&gt; fixture function scoped, which means that the &lt;code&gt;LiveServer&lt;/code&gt; class will stop after each test case, joining all request threads.&lt;/p&gt;
&lt;p&gt;This could be achieved by overriding it in my project conftest, copying the relevant contents from the pytest-django source:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django.core.servers.basehttp&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;ThreadedWSGIServer&lt;/span&gt;

&lt;span style="color: #7e8aa1; font-weight: bold; font-style: italic"&gt;@pytest&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;fixture(scope&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;function&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;live_server&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(request:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;pytest&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;FixtureRequest):&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;&amp;quot;&amp;quot;&amp;quot;Run a live Django server in the background during tests.&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    Copied from pytest-django source. Simplified based on project usage.&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span style="color: #7e8aa1"&gt;# wait on requests to finish before shutting down the server&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;ThreadedWSGIServer&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;daemon_threads&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;False&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;server&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;live_server_helper&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;LiveServer(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;localhost&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;yield&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;server&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;server&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;stop()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Put together, this approach should resolve the underlying race condition: at the end of each test, the http server will be stopped, waiting for pending requests to finish. When the database flush runs, there will be no concurrent access.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;I am on vacation currently!&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="programming"></category></entry><entry><title>Greece</title><link href="https://blog.tmk.name/2025/09/16/greece/" rel="alternate"></link><published>2025-09-16T00:00:00-04:00</published><updated>2025-09-16T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-09-16:/2025/09/16/greece/</id><summary type="html">&lt;p&gt;I am currently writing this from a breezy apartment in Heraklion, Greece. I'm spending a little over two weeks in Greece, traveling solo at the start and having a friend join me later.&lt;/p&gt;
&lt;h2&gt;Itinerary&lt;/h2&gt;
&lt;p&gt;I worked with Claude on my travel plans, starting at a high level (deciding on Greece …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I am currently writing this from a breezy apartment in Heraklion, Greece. I'm spending a little over two weeks in Greece, traveling solo at the start and having a friend join me later.&lt;/p&gt;
&lt;h2&gt;Itinerary&lt;/h2&gt;
&lt;p&gt;I worked with Claude on my travel plans, starting at a high level (deciding on Greece as the destination) and then diving into specifics (places within Greece, timeline of stays). As a travel partner, Claude / LLMs are great - not always accurate, but typically provide a good enough starting point to figure it out. Beyond planning I've leveraged LLMs as makeshift tour guides, as they're generally more informative than museum placards, and social etiquette coaches, especially helpful as an American in Europe.&lt;/p&gt;
&lt;p&gt;For 16 days in Greece, here's my itinerary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Athens: 3 nights&lt;ul&gt;
&lt;li&gt;Settle in, visit Acropolis and museums.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Chania: 3 nights&lt;ul&gt;
&lt;li&gt;Explore the town + harbor, day trip to Balos Lagoon.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Heraklion: 3 nights&lt;ul&gt;
&lt;li&gt;Visit museums and Knossos.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Naxos: 4 nights&lt;ul&gt;
&lt;li&gt;Explore the island, day trip to Rina Cave.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Athens: 3 nights&lt;ul&gt;
&lt;li&gt;Prepare for departure, possible day trip to Delphi.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Something I've learned about travel is there's a huge difference between the abstract knowledge of the trip, and the actual reality of the trip. As an example, the above itinerary felt right when planning, but I now would changed up the distribution of nights, shaving time off Heraklion and Athens for a visit to another island, or extending the stay on Naxos. This is my general experience of travel, though - I don't know what the experience of the place I'm visiting is until I actually visit!&lt;/p&gt;
&lt;p&gt;For transit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fly Athens to Chania: quick flight, took the bus from the airport to downtown Chania.&lt;/li&gt;
&lt;li&gt;Bus from Chania to Heraklion: cheap (16 euros), scenic drive along the coast of Crete.&lt;/li&gt;
&lt;li&gt;Ferry from Heraklion to Naxos: experience tbd, based on other travelers' stories I picked up motion sickness medication.&lt;/li&gt;
&lt;li&gt;Ferry from Naxos to Athens: same story.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Public transit has been reliable and cheap - I haven't taken a cab ride yet (though without a phone number, I can't sign up for freenow 🙄). Public transit isn't as nice or fast as private service, but I'm in no rush this trip.&lt;/p&gt;
&lt;h2&gt;Odds and Ends&lt;/h2&gt;
&lt;p&gt;I used a luggage storage service for the first time, which was convenient as I carry two backpacks when I travel (large one behind, small one in front). Unloading my large bag was a boon as it enabled me to stay mobile and explore for a few more hours, rather than feel compelled to hunker down with my bags in a cafe.&lt;/p&gt;
&lt;h2&gt;Experiences&lt;/h2&gt;
&lt;p&gt;So far, some highlights of my trip have included the Acropolis + museum, the Ancient Agora, the Athens Archeological Museum, the old Venetian port at Chania, and a day trip to Balos Lagoon + Gramvousa.&lt;/p&gt;
&lt;p&gt;I did a tour group for the Acropolis, which I didn't find particularly moving. Most of the information I had already read before, and the guide wasn't energetic. I rank all tours against my experience at the Greenwich Observatory, where the tour guide was clearly passionate about the history and subject matter. Tour aside, the Acropolis is an impressive monument of history, of &lt;em&gt;hubris&lt;/em&gt;: not long after its completion, Athens was defeated, never to rise again to its golden age glory.&lt;/p&gt;
&lt;p&gt;I was summarily impressed by Chania - I had no expectations about Crete, having seen it only from travel vlogs. The old town is charming in the truest sense, and the harbor lit up at night is delightful. The city is exactly what I imagined a European port city to be. I also did not realize how close the beaches were, nor their quality: the water is pristine, the clearest I've ever seen. Taking the bus to Kissamos Port I realized that the beaches extend endlessly, with the waterfront covered by hotel and resort sprawl, some nestled onto idyllic cliffs. On the ferry to Balos, the striking mountainous peninsula was awesome. I laid in the water in Balos, perfectly shallow.&lt;/p&gt;
&lt;h2&gt;Insights&lt;/h2&gt;
&lt;p&gt;A week with just myself, out of my usual environs, has created plenty of space for insights and awareness into myself. Specifically about environment, I see that the grooves of my daily habits and rituals induce a kind of stupor or cloud that makes self awareness challenging. When traveling outside of my home / culture, just about every interaction and situation is new enough that I am conscientious with a kind of stark contrast to view my own behavior.&lt;/p&gt;
&lt;p&gt;I noticed that I am always going somewhere in particular, always have a destination in mind - if I am going to eat, I have the restaurant in mind, if I am at the museum, I am glued to the guide. There's a spectrum of behavior here; in the past I detailed my itineraries down to the day, while now I am more flexible with showing up and trusting that I'll figure out my activities.&lt;/p&gt;
&lt;p&gt;Ah, that word gets to the root of so much for me - &lt;em&gt;trust&lt;/em&gt;. I have to actively recollect that I was quite fearful that I would not succeed in solo travel, that I'd be miserable and come home early. And now that I have demonstrated that I am capable, my fears have aptly moved onto the next object for speculation.&lt;/p&gt;
&lt;p&gt;The common theme being constant motion, direction, activity - chasing one experience after another, fearing one phantom after another. I'm constantly going and doing, spurred on by an internal, infernal drive, an innate &lt;em&gt;dissatisfaction&lt;/em&gt; that I'm looking to resolve.&lt;/p&gt;
&lt;p&gt;Wisdom is the knowledge that enables making better choices. It can be taught, but it's not easy to accept - I think that's an aspect of the human condition, and gets at why despite vaster and more impressive knowledge than ever before, the perfect world (for an individual or collective) cannot be educated into existence. Some truths are learned through experience. In my case, it's the experience of chasing after satisfaction through external means (alternatively, chasing after resolution of my dissatisfaction through external means).&lt;/p&gt;
&lt;p&gt;Which leads to the unsatisfying conclusion that change, healing, recovery, takes &lt;em&gt;time&lt;/em&gt;, and sometimes quite a lot of it. My internal structures were woven and reinforced through decades of experiences. To imagine a quick resolution is to mistake form for content: with the perfect life circumstances (girlfriend, job, apartment, etc.) I'd be just as dissatisfied as I am today, as &lt;em&gt;I&lt;/em&gt; am the constant. Weaving new structures, unraveling the old, is a tender and delicate process, itself enjoyable as I come to realize that there is no end state anyway, that satisfaction is possible even now.&lt;/p&gt;
&lt;p&gt;A significant element to be restructured is my perception of myself and others. This trip has once again shown me that I am constantly projecting, interacting not with the person but a fantasy of my own creation. These fantasies, stories that I've written and read so many times that I became a true believer, mirror my own inner world. When I operate from a perspective of fear and insecurity, all I see in others are fear and insecurity, or their opposite, supreme and total confidence. I treat myself as if I am unique, which became humorously apparent when I thought myself the only person to ever drop a piece of cucumber at a meal - for a moment I really imagined myself to be terminally unique.&lt;/p&gt;
&lt;p&gt;By rewriting this narrative into one of commonality and shared experience I start to open myself to myself and view others as human beings with inner worlds just as rich as my own. I start to open myself to richer relationships with others, relationships that reflect and reaffirm in mutually reinforcing feedback that I'm okay, that I always have been, and I always will be.&lt;/p&gt;
&lt;p&gt;&lt;img alt="spirals interconnected" src="https://blog.tmk.name/images/spirals-interconnected.png" /&gt;&lt;/p&gt;
&lt;p&gt;I believe the creators of this artwork understood what I am talking about. Spirals interconnected - each individual an infinity, with depth and richness of existence, connected to every other, a connection that both feeds and is fed by the other; a connection small yet critical.&lt;/p&gt;</content><category term="travel"></category></entry><entry><title>Django: Index Your Generic Foreign Keys</title><link href="https://blog.tmk.name/2025/09/06/django-index-your-generic-foreign-keys/" rel="alternate"></link><published>2025-09-06T00:00:00-04:00</published><updated>2025-09-06T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-09-06:/2025/09/06/django-index-your-generic-foreign-keys/</id><summary type="html">&lt;p&gt;In Django, models may relate to another, &lt;em&gt;generic&lt;/em&gt; model, as opposed to a concrete model, via a &lt;em&gt;generic&lt;/em&gt; foreign key, which becomes self explanatory when looking at a &lt;a href="https://docs.djangoproject.com/en/5.2/ref/contrib/contenttypes/#generic-relations"&gt;sample definition&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;TaggedItem&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;Model):&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;tag&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;SlugField()&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;content_type&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;ForeignKey(ContentType,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;on_delete&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;CASCADE)&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;object_id&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;PositiveBigIntegerField()&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;content_object&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;GenericForeignKey …&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;In Django, models may relate to another, &lt;em&gt;generic&lt;/em&gt; model, as opposed to a concrete model, via a &lt;em&gt;generic&lt;/em&gt; foreign key, which becomes self explanatory when looking at a &lt;a href="https://docs.djangoproject.com/en/5.2/ref/contrib/contenttypes/#generic-relations"&gt;sample definition&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;TaggedItem&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;Model):&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;tag&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;SlugField()&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;content_type&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;ForeignKey(ContentType,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;on_delete&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;CASCADE)&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;object_id&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;PositiveBigIntegerField()&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;content_object&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;GenericForeignKey(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;content_type&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;object_id&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A TaggedItem instance may be related to any other type of model, as opposed to being restricted to a specific type of model, say a Post. This makes the TaggedItem model quite flexible, allowing reuse across different purposes. The flexibility comes at the drawback of performance and implementation, typically.&lt;/p&gt;
&lt;p&gt;In reviewing query performance at work, I noticed that a few of the most time consuming queries (where total time = num calls * mean time) involved generic foreign key filters. Running the queries through the postgres query explainer, I was surprised to discover that generic foreign keys are not indexed by default. That is, a query that searches for TaggedItems by their related content object like:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;TaggedItem&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;objects&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;filter(content_type__pk&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;bookmark_type&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;id,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;object_id&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;bookmark_id)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;will do a full table scan to find the relevant TaggedItem rows. If you're lucky, the query will leverage an index on content_type to filter down the rows to scan, which may or may not make an impact depending on the characteristics of the table (e.g. if there's only one content type in the table, there's nothing to be gained via that index).&lt;/p&gt;
&lt;p&gt;Adding an index on content_type, object_id will avoid the scanning, reducing performance in my local test by ~300x.&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;class&lt;/span&gt; &lt;span style="color: #73D0FF"&gt;Meta&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;indexes&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;[&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;models&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;Index(fields&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;[&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;content_type&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;object_id&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]),&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To Django's credit, this behavior is called out in &lt;a href="https://docs.djangoproject.com/en/5.2/ref/contrib/contenttypes/#generic-relations"&gt;the docs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unlike for the ForeignKey, a database index is not automatically created on the GenericForeignKey, so it’s recommended that you use Meta.indexes to add your own multiple column index. This behavior may change in the future.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;though only in recent versions. If your project is older, like mine, or you weren't aware of this behavior and haven't been conscientiously adding in the indexes manually, then you may want to take a look at your queries for a potentially simple optimization.&lt;/p&gt;
&lt;p&gt;May your generic foreign key searches be fast and efficient.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Samsara</title><link href="https://blog.tmk.name/2025/08/26/samsara/" rel="alternate"></link><published>2025-08-26T00:00:00-04:00</published><updated>2025-08-26T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-08-26:/2025/08/26/samsara/</id><summary type="html">&lt;p&gt;I'm not sure where this post will go. I want to organize and record some of my thoughts that are taking shape, in the process of taking shape, hence not quite coherent yet. I hope this helps me to synthesize my experiences and understanding of myself.&lt;/p&gt;
&lt;h2&gt;Wandering&lt;/h2&gt;
&lt;p&gt;Buddhists describe &lt;em&gt;samsara …&lt;/em&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm not sure where this post will go. I want to organize and record some of my thoughts that are taking shape, in the process of taking shape, hence not quite coherent yet. I hope this helps me to synthesize my experiences and understanding of myself.&lt;/p&gt;
&lt;h2&gt;Wandering&lt;/h2&gt;
&lt;p&gt;Buddhists describe &lt;em&gt;samsara&lt;/em&gt; as wandering, in the sense of aimlessly and blindly walking the same circuit over and over again. I relate to this concept strongly: I am becoming aware of the trends in my friendships, relationships, work, and so on. There's patterns to my behavior, patterns that I live and breathe without awareness, many of which cause me suffering large and small.&lt;/p&gt;
&lt;p&gt;I've long struggled with the notion that I am not in control of my own life. I've related to the drifters, the explorers, the adventurers, the wanderers: people whose control over their life is demonstrated in their continuous motion. I've related to the underdogs, the heroes who've overcome impossible situations: people who've beat their fate through willpower, determination, courage.&lt;/p&gt;
&lt;p&gt;It's come as a revelation that outward displays of overcoming do not necessarily imply resolution of inner troubles. I think there may be a correlation there, in fact: the more that control is sought in the outside world, the less there is in the inner world; in other words, the more striving, the more inner pain.&lt;/p&gt;
&lt;h2&gt;Reenactment&lt;/h2&gt;
&lt;p&gt;I learned the term &lt;em&gt;trauma reenactment&lt;/em&gt; in therapy recently. I felt another piece of the puzzle had fallen into place. It's painfully (emphasis on &lt;em&gt;painfully&lt;/em&gt;) obvious that so many of my patterns of behavior stem from my childhood. It's a challenge for me to build and maintain relationships, stemming from a persistent cosmic background anxiety that perpetuates a state of fear, simple but powerful fear of rejection and abandonment.&lt;/p&gt;
&lt;p&gt;I have understood for some time that I am attracted to what I know; where what is known may or may not be healthy or wholesome for me. Easy example is food, I tend to eat the food of my culture, and within that I tend towards the same meals and food items for the most part; new or "exotic" foods don't quite taste right until they've been repeatedly ingested. A not so easy example is relationships: raised in a family where my needs were not met, I feel comfortable in my adult relationships where my needs are not met, and uncomfortable when they are met. How's that for causing my own suffering!&lt;/p&gt;
&lt;p&gt;The key insight from trauma reenactment is not the repetition of behaviors generally, nor the repetition of harmful behaviors specifically, but in the rationalization of the behaviors themselves. While I am engaged in unhealthy behavior, that is causing me suffering, I believe I am acting in my best interest, in a healthy way - more than that, I believe that I am resolving and overcoming my original traumatic experiences. I am living in both the past and the present at the same time, merging them together, merging the people together, responding to my girlfriend as if she were my mother, responding to my boss as if he were my father.&lt;/p&gt;
&lt;p&gt;The example from my therapist is of someone who experienced sexual abuse as a child. As an adult, they engage in sex work to feed their drug habit, and their mindset is that they have the power over their clients because they are being paid. As a child they were powerless, now they have the power - all the while engaged in dangerous and self-destructive behavior; they have internalized the anger directed at them and replay it against themself.&lt;/p&gt;
&lt;h2&gt;Resolution&lt;/h2&gt;
&lt;p&gt;Speaking with a close friend of mine, she shared that she feels she's working through her past through her current relationships: an older male parallels her father, an older woman parallels her mother. Something clicked when I talked with her: trauma reenactment is the psyche's instinctual attempt to heal from the psychological wounds of the past.&lt;/p&gt;
&lt;p&gt;To heal, there is me, and the environment. I have spent the vast majority of my time reproducing unhealthy environments for myself, working within my "comfort" zone. In the past few years I've shifted into healthier environments, friendships, relationships, while shifting out of unhealthy environments. The healing power of my environments is slow and subtle, molecular.&lt;/p&gt;
&lt;p&gt;For "me", I struggle to find the words to describe my experience. I can point to external changes easily. I don't know how to point to inner changes. "By their fruit ye shall know them" - I can point to the differences in my behavior, characterized most evidently by the changes I've made to my environment. But now I've come full circle. Ah, circles.&lt;/p&gt;
&lt;p&gt;I don't have the words to describe my experiences. I just have a hunch, I have an intuition. Something about me is different today than several years ago. Something significant and profound, but not radical - a tipping point, a critical state, a what-have-you. At some point I became willing and able to change (or was I always able, but not yet willing? or was I able and willing, but waiting for the external spark?), at some point I dipped my toes into someplace new.&lt;/p&gt;
&lt;p&gt;In this new environment, with this new me, the old me has been able to work through the old environments.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Challenges</title><link href="https://blog.tmk.name/2025/08/10/challenges/" rel="alternate"></link><published>2025-08-10T00:00:00-04:00</published><updated>2025-08-10T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-08-10:/2025/08/10/challenges/</id><summary type="html">&lt;p&gt;Several weeks ago I heard someone say, "God gives us challenges the moment we're ready for them." I can't say that I am thrilled about this, as I'd appreciate a reprieve now and again. But I have come to see the truth of it these past couple months.&lt;/p&gt;
&lt;h2&gt;Sickness&lt;/h2&gt;
&lt;p&gt;I've …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Several weeks ago I heard someone say, "God gives us challenges the moment we're ready for them." I can't say that I am thrilled about this, as I'd appreciate a reprieve now and again. But I have come to see the truth of it these past couple months.&lt;/p&gt;
&lt;h2&gt;Sickness&lt;/h2&gt;
&lt;p&gt;I've had a rough time recently. I came down with a severe case of the flu at the start of July. Initial sinus infection symptoms faded in a week, but fatigue and brain fog persisted for more than a month. Ugh. I have never been this sick for this long before. I've never been so limited in participation in my life.&lt;/p&gt;
&lt;p&gt;The matter of the sickness itself was half the battle, the other half my panic at the longevity: what if the fatigue &lt;em&gt;never&lt;/em&gt; goes away? What if I have these limitations forever? I struggled emotionally to cope, the more so as time went on. I am grateful for the support of my friends who helped me during this period.&lt;/p&gt;
&lt;h2&gt;Anxiety&lt;/h2&gt;
&lt;p&gt;Immediately preceding the illness, I was under an enormous amount of stress at work and in my personal life. I began dating a woman with whom I started falling back into old patterns of behavior. On the heels of a challenging work project I faced a test of my management skills. These scenarios played out over several weeks.&lt;/p&gt;
&lt;p&gt;Stress intermingled with the illness, I felt like I couldn't breathe. Some small and strange phenomena was that in my low-energy days my emotions were mellow; on better days, my emotions came back like roaring waves crashing over me on a day of rough surf. I couldn't catch a break.&lt;/p&gt;
&lt;h2&gt;Recovery&lt;/h2&gt;
&lt;p&gt;I am writing on a Sunday, having had the most lively weekend in over a month. On Friday, I attended a friend's performance in a community production of Matilda. Yesterday, I went kayaking with a few close friends, spending several hours on the water. Today, I went on a hike with a new friend. I feel like I'm back at full performance.&lt;/p&gt;
&lt;p&gt;Will I give myself a chance to breathe? I'm torn between feeling like I need to make up for lost time (a July spent on the couch!) and continuing to invest in my rest and recovery. As I learned about fatigue, over-doing it on high-energy days is a classic trap that lays the seeds for future crashes.&lt;/p&gt;
&lt;h2&gt;Humility&lt;/h2&gt;
&lt;p&gt;These past weeks have been humbling. The five remembrances include, "I am not immune from sickness; I will get sick." Hearing this I thought, all well and good, some day I'll get old and sick, but that's a problem for old and sickly me. I am young and healthy me, I bounce back fast from sickness. Well, at 31 years old, I feel the truth of this remembrance. I am of the nature to get sick, severely sick.&lt;/p&gt;
&lt;p&gt;The fatigue was especially painful. In the past I've been able to push myself as hard as I need to, and I've long taken for granted my ability to live my life as physically as I've so wanted. With fatigue, there were days I couldn't move off the couch, no matter how much I wanted to. The brain fog has had me forgetting names, places, memories. My job is intellectual and my inability to perform at my usual level deeply unsettled me.&lt;/p&gt;
&lt;p&gt;I am thankful for the opportunity to face this challenge. Despite years of working to curb my ego, a little scratch reveals it, seemingly bigger than ever - but I just take that to be my growing awareness seeing more of my inner parts than ever before. I'm thankful for the experience and my ability to navigate it successfully. I'm thankful for all of the challenges that I've navigated recently, some that I'm more proud of than others.&lt;/p&gt;
&lt;p&gt;I am most of all thankful for the loving support that I received from my friends.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Travels with Charley</title><link href="https://blog.tmk.name/2025/07/24/travels-with-charley/" rel="alternate"></link><published>2025-07-24T00:00:00-04:00</published><updated>2025-07-24T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-07-24:/2025/07/24/travels-with-charley/</id><summary type="html">&lt;p&gt;It's been a long July for me. The past few weeks I've experienced passion, anxiety, sickness, humility, and deep connection. As I've reduced my pace, I've dedicated myself to reading more. This is the first month I've read three books since last July - a funny pattern to appear. I read …&lt;/p&gt;</summary><content type="html">&lt;p&gt;It's been a long July for me. The past few weeks I've experienced passion, anxiety, sickness, humility, and deep connection. As I've reduced my pace, I've dedicated myself to reading more. This is the first month I've read three books since last July - a funny pattern to appear. I read &lt;em&gt;Thinking in Systems&lt;/em&gt;, &lt;em&gt;Look to Windward&lt;/em&gt;, and just finished &lt;em&gt;Travels with Charley&lt;/em&gt; by John Steinbeck.&lt;/p&gt;
&lt;p&gt;I've long admired Steinbeck. I remember once checking out several of his books at once from the college library, dutifully stacking them by my bed: &lt;em&gt;The Pearl&lt;/em&gt;, &lt;em&gt;The Winter of our Discontent&lt;/em&gt;. I remember reading &lt;em&gt;Of Mice and Men&lt;/em&gt;, and &lt;em&gt;East of Eden&lt;/em&gt; - the latter to impress a college girlfriend. &lt;em&gt;The Grapes of Wrath&lt;/em&gt; radically altered my politics at an impressionable age. I suspect revisiting these books now I'd have a completely different experience.&lt;/p&gt;
&lt;p&gt;That's all to say that I have a history with John Steinbeck, as an author. I never explored his personal life. Written towards the end of his career, &lt;em&gt;Travels&lt;/em&gt; is a story of a man trying to reconnect with his Americana roots. Places are memories, shaped by the tides of time. The gaps in the story are written in embarassment - guiltily, as if to avoid the pain of owning his own changes from scrapping youth to fame and success. There's a contradiction between rich author staying in a luxurious hotel to camping by the riverside in Rocinante.&lt;/p&gt;
&lt;p&gt;I enjoyed the book and found myself drifting in my imagination on my own road trip across the country. I've previously attempted two and completed one cross country road trip. At 17, desperate to get away, I set out from New Jersey - but never made it out of Pennsylvania, felled by the first road block. A second go in my early 20s succeeded, when I joined my friend moving to SoCal. We cleared the country in 4 days, going so fast as to overheat the engine. As Steinbeck says, sticking to the highways I can't say that I saw the country, just a shifting landscape.&lt;/p&gt;
&lt;h2&gt;The nature of trips&lt;/h2&gt;
&lt;p&gt;Steinbeck says that a trip takes the traveler more than the traveler takes the trip. I think this may be true. I've experienced hints of it over the years. I notice that, leaving the house, a subtle turn overtakes me: I attune to being on a trip, my rhythms adjust to the variations. When I return, I feel somehow completely different and exactly the same, as I embrace my home coffee maker and rocking chair. A trip subjects me to itself, its will is in charge, not mine - I am a passenger in the true sense.&lt;/p&gt;
&lt;p&gt;I also relate to how trips may end before returning home. On my trip to Thailand, I recall distinctly arriving in Bangkok for the last leg, and being absolutely sure I could go home that instant and not be sorry for it. I was done, I had seen all I could possibly see, experienced as many highs and lows as possible: my tank was on empty. On the other hand, other trips beg for more, leave me aching to return, to see more, do more, experience more. I felt this way in London, not getting to do more than half of what I had planned - and that being a fraction of all available to do!&lt;/p&gt;
&lt;h2&gt;Mass production&lt;/h2&gt;
&lt;p&gt;A common observation of Steinbeck is the changing of the cityscapes. As he describes, cities are ringed with wrecked and decaying cars and such detritus of mass production. The old city centers are hollowed out, surrounded by suburbs teeming with wealth and investment. He doesn't lament the changes; rather, throughout the book, he accepts that change is inevitable, neither good nor bad, just a fact of being old enough to remember when things were different.&lt;/p&gt;
&lt;p&gt;Another common observation is the impact of mass produced goods. Breakfast, he notes, is always freshly prepared and delicious, while plastic wrapped dinners are bland and hardly worth eating. Goods then trend towards a tolerable minimum quality, they're standardized in their blandness. The same applies to culture: as radio and television spread a mass produced culture, regional variations in speech and behavior start to fade. What was noticeable in 1960 can only be verified in 2025, with the internet erasing every last barrier to connection between communities.&lt;/p&gt;
&lt;p&gt;On this last point I can't help but reflect on my own life. When I need anything I reach immediately for Amazon and have the item next day. The information I receive through media content on the television, phone and computer are bland, harmless and without meaning. There's little real in my life, not mediated through safety filters and quality control. It's not all bad, I am grateful that I have clean water and safe food to eat. But I think the overproduction of safety and comfort has come at a price.&lt;/p&gt;
&lt;h2&gt;America&lt;/h2&gt;
&lt;p&gt;In his humble quest, Steinbeck set out to find the real America. By his own admission, it was an impossible quest: he talked to maybe a few dozen individuals out of countless millions. I am amazed at the similarities between his observations in 1960 and today. Maybe most notably, the politic divisions. He describes family political disagreements as a kind of civil war in intensity - something that cuts against the narrative that recent times are the most political (a recurring theme, I guess, kept alive through media agitation and political interests).&lt;/p&gt;
&lt;p&gt;I am fully American, I don't have roots outside the country. I can trace my ancestry back to the revolutionary war. When I think of the country, I think of opportunity, as a shared belief. It's a land of immigrants cutting ties and starting over. And within the country itself, cutting ties and starting over - it's a big country after all. Maybe prosperity is the shared belief, shared myth, that binds us together. I promise that I don't know either.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>Thinking in Systems</title><link href="https://blog.tmk.name/2025/07/12/thinking-in-systems/" rel="alternate"></link><published>2025-07-12T00:00:00-04:00</published><updated>2025-07-12T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-07-12:/2025/07/12/thinking-in-systems/</id><summary type="html">&lt;p&gt;I am wrapping up &lt;em&gt;Thinking in Systems&lt;/em&gt; by Donella Meadows, having been inspired recently to study the importance of feedback loops. I find the book fascinating and easily applicable to systems large and small, from the economy to my internal emotional world.&lt;/p&gt;
&lt;h2&gt;Systems&lt;/h2&gt;
&lt;p&gt;Systems are made up of &lt;em&gt;stocks&lt;/em&gt;, think …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I am wrapping up &lt;em&gt;Thinking in Systems&lt;/em&gt; by Donella Meadows, having been inspired recently to study the importance of feedback loops. I find the book fascinating and easily applicable to systems large and small, from the economy to my internal emotional world.&lt;/p&gt;
&lt;h2&gt;Systems&lt;/h2&gt;
&lt;p&gt;Systems are made up of &lt;em&gt;stocks&lt;/em&gt;, think of it as some resource that can be measured, like water in a bathtub. Stocks are affected by &lt;em&gt;flows&lt;/em&gt;, analagous to the pipes that feed water into or out of the tub. The simplest systems have no relation between the stocks and the flows, eg the amount of water in the tub does not affect how fast the tub can drain.&lt;/p&gt;
&lt;p&gt;When stocks and flows are interconnected, there is &lt;em&gt;feedback&lt;/em&gt; within the system. Think of an interest bearing bank account, where the balance yields interest, which compounds itself: the rate of return, rather than being constant, is a function (typically, a percentage) of the balance. This is a reinforcing feedback loop, which demonstrates exponential growth, ie the stock grows faster and faster over time (in fact, &lt;em&gt;really fast&lt;/em&gt; over not so much time!). Balancing feedback loops tend towards some goal, as in the case of a thermostat in a house. When the temperature goes below the threshold, the heating unit kicks in; when the desired temperature is reached, it stops. The stock is kept at the target.&lt;/p&gt;
&lt;h2&gt;Large and small&lt;/h2&gt;
&lt;p&gt;The book makes the case that infinite growth cannot be sustained in a finite system. The author is making the argument against infinite economic growth on a planet with finite resources. In the ecological vein, fossil fuels cannot last forever; in simple models, exponential growth in depletion can lead &lt;em&gt;very quickly&lt;/em&gt; to resource exhaustion. In my mostly uninformed knowledge, the world has kept up with oil demand through discovering new means of extraction - the USA is the world's largest oil producer thanks to shale extraction. But every finite resource has its limit, which cannot be increased as fast as demand can grow.&lt;/p&gt;
&lt;p&gt;In the small scale, I am interested in my emotional wellbeing as a system. Let's say the stock is my stress level. In my view, stress is a reinforcing feedback loop: the more stressed I already am, the more each new stressor is amplified. After meditation I won't be bothered by someone driving erratically, but when my car's AC is broken on a sweltering day and I've already been cut off a dozen times, I am likely to start honking!&lt;/p&gt;
&lt;p&gt;In the outflow, I think there's typically a balancing feedback loop towards low stress - unless my goals are miswired due to chronic stress, when my internal systems may naturally guide me towards &lt;em&gt;more&lt;/em&gt; stress than I'd like. This is a point towards reflection and awareness: is my resting state anxious or calm? In terms of actually reducing stress, another key lesson is time: stocks and feedback loops tick, they do not interop instantly. In other words, there's no instant meaningful stress reduction. Time takes time, as they say, and when coming down from a high stress level, results may not be immediately apparent. Taking a walk after a brutal day at work won't cut my stress to zero, and if I react to that by beating myself up for failing to eliminate stress, well I am just reinforcing my stress rather than reducing it! A key lesson for me is that stress relief has to be a consistent practice, with results that appear sometimes slower than I'd like.&lt;/p&gt;
&lt;h2&gt;Catastrophe&lt;/h2&gt;
&lt;p&gt;In &lt;em&gt;Ubiquity&lt;/em&gt; by Mark Buchanan, the author explores catastrophes, and challenges the prevailing wisdom that significant outcomes require significant inputs, ranging from natural disasters like avalanches and forest fires to mass extinction events and economic collapse.&lt;/p&gt;
&lt;p&gt;Some systems, such as forests, have naturally occurring balancing feedback loops that keep dry organic material at a low level through recurring small fires. When human intervention disrupts this natural feedback loop, by policy putting out every fire, the amount of dry material builds up over time, tending towards a &lt;em&gt;critical state&lt;/em&gt;, one in which any spark can lead to a cataclysmic fire.&lt;/p&gt;
&lt;p&gt;Generalizing, I think systems modeling is useful for understanding crisis prone systems. Naturally occurring systems tend towards equilibrium, with internal and external balancing feedback loops. Understanding crisis as a disruption to those feedback loops is helpful to understand how to address the problem: sometimes the feedback loop needs to be restored, sometimes the system needs to be remodeled entirely because the conditions it evolved under have changed.&lt;/p&gt;
&lt;p&gt;In the case of evolution, Buchanan describes a simple mathematical model in which evolution can lead to its own critical state, in which a small change to one specie's success can cascade quickly into a mass extinction event - no asteroid necessary! Similarly with capitalism, the boom and bust cycle is not caused by outside intervention but rather the internal dynamics of the system itself, leading to periodic crisis.&lt;/p&gt;
&lt;p&gt;For emotional wellbeing, burnout is one potential catastrophe that results from unmanaged and accumulated stress. It may manifest as the result of this or that particular event, but it's likely due to the internal balancing feedback loops not working or not being able to cope with the &lt;em&gt;pattern&lt;/em&gt; of incoming stress. To avoid a stress-based critical state, it behooves me to ensure my perception of my stress level is accurate, and my stress flows in and out are in line with my desired stress level.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;I could go on and on, I find this subject to be endlessly fascinating and applicable. To keep it short, systems are a useful way to model the world - but remember, they're only models, and therefore inherently limited and flawed. Still, approximations can be used to incredible effect, and a strong application of systems thinking can provide actionable insights into a problem.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>AI (2025)</title><link href="https://blog.tmk.name/2025/06/09/ai-2025/" rel="alternate"></link><published>2025-06-09T00:00:00-04:00</published><updated>2025-06-09T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-06-09:/2025/06/09/ai-2025/</id><summary type="html">&lt;p&gt;I haven't written much about AI, just a &lt;a href="https://blog.tmk.name/2025/01/10/closed-systems/"&gt;post lamenting degraded open access&lt;/a&gt; and a &lt;a href="https://blog.tmk.name/2024/06/13/code-slop/"&gt;post complaining about LLM verbosity&lt;/a&gt;. I want to share a few perspectives and predictions here, as much to summarize where I'm at on the topic, as to give myself a looking-back point in the future …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I haven't written much about AI, just a &lt;a href="https://blog.tmk.name/2025/01/10/closed-systems/"&gt;post lamenting degraded open access&lt;/a&gt; and a &lt;a href="https://blog.tmk.name/2024/06/13/code-slop/"&gt;post complaining about LLM verbosity&lt;/a&gt;. I want to share a few perspectives and predictions here, as much to summarize where I'm at on the topic, as to give myself a looking-back point in the future.&lt;/p&gt;
&lt;h2&gt;Experience&lt;/h2&gt;
&lt;p&gt;I use LLMs at work on a daily basis. The primary interface is through copilot autocomplete in my Emacs editor. This is a love-hate relationship, when the suggestion frequency is dialed too high, the bad autocompletes generate more cognitive disruption than good autocompletes save cognitive effort and typing time. That said, I've never disabled it.&lt;/p&gt;
&lt;p&gt;I also use &lt;a href="https://github.com/karthink/gptel"&gt;gptel&lt;/a&gt;, typically in dedicated chat buffers, and when I remember using in-buffer rewrites. I usually create ephemeral chat buffers, but have started using file-backed buffers for persisting chats for later recall. I find that gptel has a great balance of usability and power; like my favorite tools it stays out of the way, has simple shortcuts for the common use cases, and leverages transient.el for powerful configuration.&lt;/p&gt;
&lt;p&gt;I trialed cursor.ai, and can't wait for suggested-next-edit to come to Emacs. I also briefly demo'd claude code, and plan to invest more time with agentic coding.&lt;/p&gt;
&lt;h2&gt;Predictions&lt;/h2&gt;
&lt;p&gt;These are by no means "hot takes" - as I've disconnected from the internet's overwhelming stream of consciousness, I've lost interest in such things! Instead, the predictions are meant to organize some of my thinking about how I use LLMs today and what I expect in the near future.&lt;/p&gt;
&lt;h3&gt;Degraded internet&lt;/h3&gt;
&lt;p&gt;We've already seen major internet websites like X and Reddit close down their previously generous API access. At first, I suspected this trend to continue, as platforms hoard their precious data in order to sell to the highest bidder for training data.&lt;/p&gt;
&lt;p&gt;With the rise of agents, or "tool-using LLMs", or "function-calling LLMs connected to other software systems", or really just "LLMs generating structured output", I predict major platforms will be forced to step up their protections, extending beyond API access to the basic web and app interfaces that humans use. This is because agents can act on behalf of humans, interfacing with dynamic websites about as well as a human can, but importantly not generating the income from human eyeballs on the omnipresent advertisements.&lt;/p&gt;
&lt;p&gt;As an example, imagine an agentic booking a vacation for me, intelligently skipping ads and promoted content. This threatens the foundation of the internet advertising model.&lt;/p&gt;
&lt;p&gt;What could the future hold? Maybe humans will be forced to have their cameras on, and eyes tracked, as if taking a remote examination on their computer. That's possible, but unlikely. It's much easier for me to imagine advertising to adapt and evolve, maybe injecting ads into LLM training data, or returning prompt injections when accessed by LLMs.&lt;/p&gt;
&lt;p&gt;It's a tragedy. I predict for a brief window we'll see just what technology could do if platforms were open and inter-operable, if personal data were accessible, if my computer and software worked &lt;em&gt;for me&lt;/em&gt; and not &lt;em&gt;for others&lt;/em&gt;. Agents are a crowbar forcing open the door, but in short time stronger walls will be built.&lt;/p&gt;
&lt;p&gt;At the end of the day, it's all about the money. The incentives are in place to hoard data, lock down and gate access, and prevent the usefulness of agents, in order to force humans to interface with monopolistic corporations, so that profit can be made.&lt;/p&gt;
&lt;h3&gt;Voice first&lt;/h3&gt;
&lt;p&gt;Generating structured output from unstructured input is an incredible feature of LLMs. I predict this will yield a wave of new user interfaces that are voice-first. I think back on all the frustrating, multi-step, annoyingly custom forms that I submit on a daily basis. What if that could be avoided, or at least alleviated, through a friendly chat-like interface? How nice if I could blabber on for a few minutes and have the data nicely structured for me - no formatting dates or phone numbers incorrectly, for one.&lt;/p&gt;
&lt;p&gt;At it's simplest, imagine a browser extension that supports voice input to fill in forms throughout the internet. Given use over time, it should be able to learn/remember my data and auto-complete most forms, asking me only the unique information in the form.&lt;/p&gt;
&lt;p&gt;This is one example of a broader possibility that I foresee. There's an incredible diversity of data entry UIs on the internet. Every time I work with a designer on a new form, it invariably includes new and unique form inputs. I think this is a waste of time and cognitive effort for users, who have to contend with yet-another-dropdown-that's-not-like-any-dropdown-I've-seen-before.&lt;/p&gt;
&lt;p&gt;LLMs can present a simplified and unified interface to diverse form entries, saving a not-insignificant amount of time for users.&lt;/p&gt;
&lt;p&gt;Who knows, maybe this will provide back-pressure against such complex and bespoke interfaces? Or websites will provide LLM back-channel interfaces and integrations specifically designed for the dominant LLM and agent workflows.&lt;/p&gt;
&lt;p&gt;Just as above, I suspect this pleasant future will not come easily, and there'll be conflict between LLM automation tools and major providers. User interfaces are subject to the same incentives, and so any attempt to bypass them will be fought tooth and nail. Further, UIs are the frontlines of the battle for user attention, subject to insane levels of control and manipulation.&lt;/p&gt;
&lt;h3&gt;Agentic coding&lt;/h3&gt;
&lt;p&gt;As a software engineer, this one hits home most closely. Will I have a job in a few years? If I do, what will it look like?&lt;/p&gt;
&lt;p&gt;I'll state that I do expect to continue working as a software engineer for the foreseeable future. I suspect my day to day will change - I got a &lt;a href="https://changelog.com/friends/96"&gt;taste of the future of babysitting agents in a recent podcast&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Agents are great at validating whatever you put into them. That is great, if I am keyed into the right idea or perspective, but if I am not, I am liable to dig in deeper into the wrong approach. For software, where engineers love to write new green-field code, I suspect there will be an explosion of new code that's poorly vetted.&lt;/p&gt;
&lt;p&gt;As we know, reading code is significantly harder than writing code, especially when not in your style or a known colleague's style. As agents become code authors and not merely code assistants, the skill of reading code will become even more valuable. As agents then review and release code independently, it'll be the task of the overseeing software engineer to monitor and ensure the systems are running correctly - just like humans, agents have a tendency to skip important requirements!&lt;/p&gt;
&lt;p&gt;Maybe this means software engineers working alongside or above a "team" of agents. But to suggest this future also implies an explosion in code changes that need to be vetted, and new software systems that need to be overseen - suggesting a need for at least as many, if not more, engineers.&lt;/p&gt;
&lt;p&gt;I'd be remiss not to mention the security problems of LLMs. Agents inherently mix command and data channels, meaning there's no possibility of safely passing in untrusted data. The average sentiment appears to be, as usual for security, "who cares?" and that will remain the case until and well after the major security breaches start. When I run an agent with full autonomy (i.e. all permissions enabled, full system access via tooling), I am extending my trust to every possible source of input into the LLM.&lt;/p&gt;
&lt;p&gt;For example, if I instruct my agent to use a particular library to solve a problem, it's easy to conceive the agent searching for relevant documentation, ending up on a page with an embedded prompt injection that steals files, code, or production API keys.&lt;/p&gt;
&lt;p&gt;In production, the threat is much higher - I heard a story about a company's production LLM given a tool to &lt;em&gt;execute&lt;/em&gt; code.&lt;/p&gt;
&lt;p&gt;Beyond security, there's the practical questions of running multiple agents - because they're automated, of course I'd run as many as I can. It's a small prediction, but I foresee that reproducible, isolated environments will thrive. Perhaps we'll also see a resurgence of the "develop in the cloud", and big providers which invested in this area will get their payday if they can execute well.&lt;/p&gt;
&lt;p&gt;At work, it takes a couple hours of annoyingly manual work to get set up and running with a local development environment. Imagine if I wanted to spin up a dozen cloud environments with agents hacking away - it would be incredibly challenging, and any updates to said environments would be quite slow.&lt;/p&gt;
&lt;p&gt;Instead, suppose that an environment can be spun up in a few minutes or even seconds - I'm talking compiled single binary, sqlite in memory database, a totally isolated and reproducible environment. Then agents could be spun up on-demand and put to work, and die (or, retire?) when they're done. That seems like a compelling advantage.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Wrapping up, I am excited for the future of software, both as a user and as a programmer. My over-arching thesis is that the current economic situation will hold back the full potential of LLMs, as monopolistic companies desperately leverage their market domination to hold onto their stock values. As an engineer, I am excited to move up the abstraction layer, and become more productive with agentic tooling.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Stacking</title><link href="https://blog.tmk.name/2025/06/01/stacking/" rel="alternate"></link><published>2025-06-01T00:00:00-04:00</published><updated>2025-06-01T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-06-01:/2025/06/01/stacking/</id><summary type="html">&lt;p&gt;I believe strongly in merging small code diffs. In my experience, small code diffs merge more quickly, are released more quickly, leading to a faster feedback loop with end users - reducing the probability that work is invested into the wrong software.&lt;/p&gt;
&lt;p&gt;There are other benefits: smaller code diffs are easier …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I believe strongly in merging small code diffs. In my experience, small code diffs merge more quickly, are released more quickly, leading to a faster feedback loop with end users - reducing the probability that work is invested into the wrong software.&lt;/p&gt;
&lt;p&gt;There are other benefits: smaller code diffs are easier to review. Past a dozen files or a few hundred lines changed, the feasibility and quality of any code review plummets. Likewise for quality assurance: smaller diffs affect fewer systems, and are typically significantly easier to test, leading to lower risk for releases - decreasing the potential "blast radius" as my coworker calls it.&lt;/p&gt;
&lt;h2&gt;Stacking&lt;/h2&gt;
&lt;p&gt;This all sounds well and good, but is challenging in practice. Fixing a null pointer bug with a null check may be a line or two of code, but what about creating a brand new feature, or performing a large scale refactoring? How can I transform large code diffs into smaller ones?&lt;/p&gt;
&lt;p&gt;One battle tested solution I use is code diff stacking, or PR stacking. What this basically means is a chain of git branches, a stack of code diffs that build on top of each other. In other words, break down the code diff across multiple diffs. For example:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;develop &amp;lt;-- feature/friends-list-api &amp;lt;-- feature/friends-list-frontend
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Where the first branch builds a web API, and the second builds the frontend consuming the API.&lt;/p&gt;
&lt;h2&gt;Why not one PR?&lt;/h2&gt;
&lt;p&gt;This is a good question! Sometimes, one PR is the right approach. I use these heuristics when approaching the question:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How many files changed? More than a dozen: consider stacking.&lt;/li&gt;
&lt;li&gt;How many lines changed? More than a few hundred: consider stacking.&lt;/li&gt;
&lt;li&gt;How many systems affected? More than a few: consider stacking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I would emphasize that stacking or splitting diffs is not a dogmatic approach, but a pragmatic approach, a tool to be used in the right circumstances.&lt;/p&gt;
&lt;h2&gt;Break it down&lt;/h2&gt;
&lt;p&gt;I typically approach stacking with the following approaches.&lt;/p&gt;
&lt;h3&gt;Vertical&lt;/h3&gt;
&lt;p&gt;Vertical stacking means each code diff is fully functional and can be released independently. This is an ideal approach when building an entirely new feature, which can be gated behind a feature flag.&lt;/p&gt;
&lt;p&gt;For instance, I was tasked to build a new dispatchers page at work. This page was fairly dynamic, with a master/detail view of live driver routes, plus actions to handle on the road scenarios. I'll note there was a lot of uncertainty with this feature, so getting users on board by releasing early and often is key.&lt;/p&gt;
&lt;p&gt;I broke the code diff down as follows. First, the master view, a pane displaying a list of driver routes. This was straightforward to build, as I only had to focus on building a solid foundation and displaying the list. Then, in a second diff, I built the detail view for the driver route. On top of that, more diffs followed, iterations on functionality and bug fixes from the previous diffs. This encouraged a strong feedback loop between users and devs, avoiding the unfortunate scenario where months of work go into a release that's ultimately rejected by users.&lt;/p&gt;
&lt;h3&gt;Horizontal&lt;/h3&gt;
&lt;p&gt;As opposed to vertical slices, horizontal slices are not independently releasable. Instead, the code diff is broken down into logical pieces, system by system; the small code diffs are merged together and then released all at once.&lt;/p&gt;
&lt;p&gt;This can mean different things in different codebases and projects. A typical example is breaking down a feature into data model changes, followed by frontend changes, followed by backend changes, followed by internal changes. This is the approach I am currently taking for a project that's upgrading a previous feature, extending it to offer more options to the customer.&lt;/p&gt;
&lt;p&gt;The first diff in my stack implements the database changes, with care paid to migrating existing data to new columns. The next builds out the updated customer user interface, with accompanying API changes. The next diff addresses necessary changes to internal operations systems. I approached the diffs this way because the underlying data model changes necessitated significant changes to several systems, that is the first "thin vertical slice" would frontload a disproportionate amount of code.&lt;/p&gt;
&lt;h3&gt;Multi stage&lt;/h3&gt;
&lt;p&gt;When reflecting on a few recent projects at work, and in discussion with other engineers, I thought of a new approach to stacking that combines horizontal and vertical. The motivation is when, as in the previous example, the initial "thin vertical slice" would be so large as to erase any potential gains from stacking.&lt;/p&gt;
&lt;p&gt;In my example project, I realized that the customer user interface changes were backwards compatible. I could have worked on the interface changes first, aiming to release the changes, and then following the horizontal stacking for the remainder of the work. This would have meant the first code release would be significantly sooner, and the interface could be tested with customers, alongside further development.&lt;/p&gt;
&lt;p&gt;A coworker has been working on a large scale refactor of our codebase to support multiple currencies. The first code diff was quite large, and contained a slew of different refactors, including to the data model, template display, billing, and so on. In hindsight, the large diff could have been broken down similar to my project above: for example, a first diff could refactor the template display of currencies to a universal interface - &lt;code&gt;render_currency()&lt;/code&gt;. This could even be broken down further into batches of changes to templates, de-risking any individual diff. Finally, when the underlying data model is changed, that code diff can add support for rendering the new currencies by changing only one function - &lt;code&gt;render_currency()&lt;/code&gt; - rather than hundreds of templates.&lt;/p&gt;
&lt;p&gt;I think these two approaches are slightly different, and could be fleshed out further (and named?), but they share a common theme: identify changes that are in support of later code changes, either by frontloading backwards compatible changes, or refactoring to a universal interface.&lt;/p&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;p&gt;Before wrapping up, let me note a few tips.&lt;/p&gt;
&lt;p&gt;I use a rebase strategy for keeping my branches up to date. I have recently begun experimenting with single commit branches and interactive rebases to streamline this workflow further. I'll note that there are products that help with this, namely &lt;a href="https://graphite.dev/"&gt;graphite&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When requesting reviewers, I may ask the same person to review the entire stack, just as if they were requested to review one large PR.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I find these practices to be useful and productivity boosting. I feel more confident in my code when diffs are small and easy to review and test. I look forward to continuing to hone this skill.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Scope</title><link href="https://blog.tmk.name/2025/05/10/scope/" rel="alternate"></link><published>2025-05-10T00:00:00-04:00</published><updated>2025-05-10T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-05-10:/2025/05/10/scope/</id><summary type="html">&lt;p&gt;This week I was asked to justify a development estimate by a product manager. As context, the project had undergone weeks of planning and proposal, and it's now under active development by yours truly. I gave an honest estimate of about 4-6 weeks, including coding, review, and testing. This estimate …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This week I was asked to justify a development estimate by a product manager. As context, the project had undergone weeks of planning and proposal, and it's now under active development by yours truly. I gave an honest estimate of about 4-6 weeks, including coding, review, and testing. This estimate surprised the product team. The product team's surprise in turn surprised me. Why the mutual surprise?&lt;/p&gt;
&lt;h2&gt;What's in an estimate, anyway?&lt;/h2&gt;
&lt;p&gt;This was part of the line of questioning from my product manager: how does 4-6 weeks break down per component of the project? That line of questioning misses the forest for the trees. On the face of it, it makes a certain sense: at the end of the project, I could look back and count how many hours I spent coding each component. That would give a concrete and empirical answer to the question.&lt;/p&gt;
&lt;p&gt;In an estimate, I sometimes use a similar heuristic. If I am tasked to build a new feature, I could for example compose an estimate as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1-2 days coding the data model&lt;/li&gt;
&lt;li&gt;1-2 days coding the UI&lt;/li&gt;
&lt;li&gt;1-2 days review and testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Amounting to about a week timeline. This type of estimate works well, until it doesn't. In a mature codebase, like the one I work on, seemingly simple features can take weeks or months to build. The reason for this is the complexity.&lt;/p&gt;
&lt;h2&gt;Complexity is the enemy&lt;/h2&gt;
&lt;p&gt;Complexity is invisible to product and stakeholders. From the outside looking in, my car seems fairly simple: there's a steering wheel and gear stick, and some pedals by my feet. Behind the simple interface there is a swatch of complexity: imagine my surprise that replacing my clutch pad costs thousands because the transmission has to be removed to access the underlying parts. What I believed naively to be replacing a pedal is a complex mechanical operation.&lt;/p&gt;
&lt;p&gt;Similarly for non-technical folks, the cost (in terms of &lt;em&gt;time&lt;/em&gt; rather than &lt;em&gt;money&lt;/em&gt; typically) to maintain or change software features can be surprising, as the product manager can attest. The feature request - adding a second option to a selector - seems so simple! I had to explain the underlying complexity, just as my mechanic friend had to explain the same to me about my clutch. Here are some contributing factors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The initial design was not extendable, i.e. the old feature has to be removed in order to build the new. This is more challenging than building from scratch, especially when mobile application APIs have to remain backwards compatible.&lt;/li&gt;
&lt;li&gt;Several levers for customization: each option in the selector has customizable properties that affect both display and functionality.&lt;/li&gt;
&lt;li&gt;Bespoke UI: rather than leveraging the design system, there are custom UI components.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these adds considerable complexity, and therefore effort and timeline.&lt;/p&gt;
&lt;h2&gt;Talk about scope&lt;/h2&gt;
&lt;p&gt;It's my opinion that scope should be a top concern in project proposals. This is a lesson that I've taken to heart from my boss, who strongly advocates for a focus on ROI in software projects. What I've found in my experience is that it's easy to discuss scope at the outset of a project, when it's most abstract: "we want to launch this by end of the month!" But the devil is in the details when having to make hard decisions, that is say "no" to stakeholders. In the example of my current project, this could have meant eliminating some of the customization options or leveraging the design system.&lt;/p&gt;
&lt;p&gt;As an engineer, it's my responsibility to inform product about the impact to scope for these types of decisions. If there are shortcuts, or 80-20 opportunities, then I will advocate for them - as I believe a feature shipped quickly and imperfectly is better than a feature shipped late and "perfect" (but let's not kid ourselves, there's no perfect feature launch!). As it goes, I did make these suggestions during this project's proposal process, and the suggestions were rejected. Hence my surprise at their surprise.&lt;/p&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I'm in a stage of my life where I am learning to let go. I firmly believe in personal excellence, in doing the best that I can. To that end, I committed myself to delivering this project according to my estimate. But when it comes to soft pushback on the timeline, I don't feel responsible, i.e. pushing myself to work longer hours/weekends to deliver sooner. The project will be delivered when it's delivered, and if that's later than expected, maybe next time scope will be prioritized throughout the project.&lt;/p&gt;</content><category term="career"></category></entry><entry><title>Attention Footprint</title><link href="https://blog.tmk.name/2025/05/03/attention-footprint/" rel="alternate"></link><published>2025-05-03T00:00:00-04:00</published><updated>2025-05-03T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-05-03:/2025/05/03/attention-footprint/</id><summary type="html">&lt;p&gt;I attended DjangoCon EU in Dublin, Ireland last week. I got to see a few of my colleagues, and an ex-colleague, and travel throughout the British Isles. It was an excellent work + leisure trip. I came away from the conference with several takeaways. One minor lesson: don't judge a talk …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I attended DjangoCon EU in Dublin, Ireland last week. I got to see a few of my colleagues, and an ex-colleague, and travel throughout the British Isles. It was an excellent work + leisure trip. I came away from the conference with several takeaways. One minor lesson: don't judge a talk by its title! Looking ahead at the schedule, my group shared the sentiment that the talks seemed uninteresting. Still, we showed up and were very pleasantly surprised by the quality and depth of the presentations. Personally, I found a mix of both technical and non-technical wisdom.&lt;/p&gt;
&lt;h2&gt;Non-technical wisdom&lt;/h2&gt;
&lt;p&gt;One such non-technical takeaway was in the very first talk about code review in the Django project. The presenter described a common open source maintainer experience: a new contributor makes a pull request for a feature they want, and invests little-to-none in documentation and maintenance of that feature.&lt;/p&gt;
&lt;p&gt;I have found this to be a pattern in software engineering culture. It's easier for engineers to write code than it is to communicate, let alone build consensus or change processes involving people. Engineers feel safe in the realm of impersonal code and machines, in metrics and passing unit tests. Interpersonal relations have no such safety guarantees: there's no compiler check that promises a coworker will do exactly as you instructed without blowing up.&lt;/p&gt;
&lt;p&gt;But that's a topic for another day.&lt;/p&gt;
&lt;h2&gt;Attention footprint&lt;/h2&gt;
&lt;p&gt;As I interpreted the speaker, my attention footprint is the demand that I place on other's attention.&lt;/p&gt;
&lt;p&gt;A classic example is &lt;a href="https://dontasktoask.com/"&gt;asking to ask&lt;/a&gt;. A coworker messages me saying, "Hey, can I ask you a question about so and so?" and after I reply "Go ahead" they write the actual question. Typically there are several minutes of delay between message and reply, leading to rapid context switches, or just sitting idly while "X is typing...". Asking to ask puts a higher demand on my attention as opposed to writing out the desired question.&lt;/p&gt;
&lt;p&gt;In the talk, the speaker was focused on how we can reduce our demands on others' attention. As the talk was oriented around code review, there were several suggestions:&lt;/p&gt;
&lt;p&gt;Writing good pull request descriptions: the aim is to provide the necessary context to review the code. When I receive a review request for a pull request with a blank description, vague title and several dozen files of code changed, I have to go digging for the relevant context, try to divine the intention and reasoning of the changes. On the other hand, when I receive a request to review changes that are thoroughly documented, I'm able to context switch in easily and spend significantly less time reviewing.&lt;/p&gt;
&lt;p&gt;I typically perform a self-review on pull requests. I'll call out specific sections of code that are sensitive and should be reviewed more closely. I'll also describe my thought process behind decisions (in instances where a code comment would be inappropriate, e.g. choosing a third party library or refactoring a function). This precipitates questions that I expect a reviewer to ask, based on my own experience reviewing code.&lt;/p&gt;
&lt;p&gt;When I balance what I give of my attention to what I am demanding, it leads to better outcomes for me. When my pull requests are easy to review, they are reviewed more quickly. When I ask good questions, I get good answers more quickly.&lt;/p&gt;
&lt;h2&gt;Generalizing&lt;/h2&gt;
&lt;p&gt;I believe this lesson is applicable beyond code review. In my interpersonal relationships, am I listening as much as I am speaking? Am I communicating clearly, or am I expecting others to read my mind? Striking a balance, and paying attention to my attention, pays rewards in better, easier relationships.&lt;/p&gt;</content><category term="career"></category></entry><entry><title>Awakening</title><link href="https://blog.tmk.name/2025/04/30/awakening/" rel="alternate"></link><published>2025-04-30T00:00:00-04:00</published><updated>2025-04-30T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-04-30:/2025/04/30/awakening/</id><summary type="html">&lt;p&gt;I read that spiritual awakenings typically happen in one of two ways. In the more dramatic fashion, an individual has a transcendental experience with the divine. They may see a light, hear a voice, or in any case have a particularly memorable instance of connection. The other way is slow …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I read that spiritual awakenings typically happen in one of two ways. In the more dramatic fashion, an individual has a transcendental experience with the divine. They may see a light, hear a voice, or in any case have a particularly memorable instance of connection. The other way is slow, gradual and molecular. There's no particular moment of awakening.&lt;/p&gt;
&lt;p&gt;I am aware that I am experiencing this slow spiritual awakening. I realized that if five-years-ago-me heard today-me speak about my perspectives and life today, old me would think new me had lost their mind! Which always brings me back to the fantastic line from &lt;em&gt;A Christmas Carol&lt;/em&gt;, where the maid says to newly awakened Ebenezer, "You don't seem yourself!" and Ebenezer replies: "By God, I hope not!"&lt;/p&gt;
&lt;p&gt;I feel silly even writing this. I don't pretend to have gained any secret knowledge about the world. I write to record this for my future self, that I may one day look back and smile, in love and good humor at my crude and clumsy attempts to make sense of it all. I'm going to take the risk of globalizing my experiences and understandings here.&lt;/p&gt;
&lt;h2&gt;The water of life&lt;/h2&gt;
&lt;p&gt;Every individual is interconnected. (This is where I start to sound loony to my past self.) Every action affects countless beings, probably all beings. (Seriously, what am I smoking.. okay, commentary over). I believe this is related to what the zen monks mean by "everything is reflected within everything else."&lt;/p&gt;
&lt;p&gt;The metaphor that came to me is water. Each individual is a body of water, ranging from a puddle to a pond to a lake to an ocean. The size of the water represents our capacity for acceptance, resilience, equanimity.&lt;/p&gt;
&lt;p&gt;By default, the surface of the water is calm. Events disturb the calm. Jump into a puddle and it'll be thrown into chaos. Jump into an ocean and the ocean barely notices.&lt;/p&gt;
&lt;p&gt;Just so, our nature as individuals is calm. The events of life disrupt our calm. Our reactions to those disruptions ensure the calm doesn't return. The cycle begins.&lt;/p&gt;
&lt;p&gt;The bodies of water are interconnected by streams and rivers. A peacefully flowing stream may become a raging torrent, and destroy the calm of a small pond. The disruption does not end with the pond: it rages onward, radiating outward to its connected waters, to the next bodies, and on and on, dissipating but affecting everything (I recall in a physics lecture that the gravity of a body affects another body, inversely proportional to distance, trending towards zero but never exactly zero).&lt;/p&gt;
&lt;p&gt;Just so, as individuals, when we are hurt, we radiate that pain out to others. When we're unhappy or angry we make others unhappy or angry. Consider again the size of the water. A raging torrent may ravage a pond, but it dissipates into an ocean. In the same way, as individuals, when we cultivate equanimity, when we build our capacity for resilience, we no longer propagate pain to others.&lt;/p&gt;
&lt;h2&gt;Faith&lt;/h2&gt;
&lt;p&gt;It's a process. At first, we're ignorant of the cycle of pain. We hurt others as we are hurt by them. When we build some capacity for resilience, we no longer amplify or return what is given us, but dampen the effect, working to minimize our contribution. Later, we are able to stop propagating pain, and return love instead.&lt;/p&gt;
&lt;p&gt;It's taken me a long time to appreciate the truth in these words. That journey has been rooted in faith, a faith that I couldn't articulate previously. Thankfully, the monk at the monastery spoke on faith this past weekend.&lt;/p&gt;
&lt;p&gt;Change in this life is possible. It's possible to alleviate pain and suffering in this life. It's possible to heal, grow, transform. I've felt this to be true my entire life, but I have not been able to express it or truly experience it.&lt;/p&gt;
&lt;h2&gt;Humility&lt;/h2&gt;
&lt;p&gt;None of the ideas I lay claim to. After two years of attending a Buddhist monastery and spiritual fellowship, the soil of my mind has been fertilized with much wisdom, passed down from others to me. In connecting myself to these sources, to the universal source, I feel humble and blessed. I feel hopeful.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Pytest, playwright and Django</title><link href="https://blog.tmk.name/2025/04/06/pytest-playwright-and-django/" rel="alternate"></link><published>2025-04-06T00:00:00-04:00</published><updated>2025-04-06T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-04-06:/2025/04/06/pytest-playwright-and-django/</id><summary type="html">&lt;p&gt;At work, I inherited a legacy end to end testing setup based on wdio.js. It featured few tests, and wasn't run in CI, so it fell out of date pretty quickly. It also suffered from some of the usual challenges of using a non-integrated e2e testing tool, namely server …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At work, I inherited a legacy end to end testing setup based on wdio.js. It featured few tests, and wasn't run in CI, so it fell out of date pretty quickly. It also suffered from some of the usual challenges of using a non-integrated e2e testing tool, namely server setup/teardown and data fixtures. I decided to revamp the e2e test setup in order to solve these problems and encourage engineers to write more tests.&lt;/p&gt;
&lt;h2&gt;Playwright&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://playwright.dev/python/"&gt;Playwright&lt;/a&gt; ships with pytest integration, which made it an attractive candidate for testing our Django application with an existing pytest-based test suite. It's also a modern browser automation software, having learned from the painful experience of nigh-two decades of Selenium. Playwright features automatic waiting, a GUI-based test writing tool, a test inspector with play by play action, and more.&lt;/p&gt;
&lt;p&gt;I strongly believe in using as few programming languages as possible within a project. This encourages and leverages language expertise for engineers, and reduces barriers to contribution for new team members. Writing tests in python, especially in pytest, meant that engineers could contribute to e2e tests with minimal friction. It also meant code reuse with our existing pytest fixtures.&lt;/p&gt;
&lt;p&gt;With playwright in pytest, writing a test that opens a browser at a given page and runs assertions is this simple:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;re&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;playwright.sync_api&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;Page,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;expect&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;test_has_title&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(page:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;Page):&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;page&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;goto(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;https://playwright.dev/&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

    &lt;span style="color: #7e8aa1"&gt;# Expect a title &amp;quot;to contain&amp;quot; a substring.&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;expect(page)&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;to_have_title(re&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;compile(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Playwright&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Amazing! Short, sweet and to the point. &lt;/p&gt;
&lt;p&gt;For my e2e tests I want to open a browser at a local server. Certainly I could do this by opening a new terminal, running &lt;code&gt;manage.py runserver&lt;/code&gt;, and testing with &lt;code&gt;page.goto("http://localhost:8000/")&lt;/code&gt;.. but that means I'm limited in a number of ways. One, state must be pre-configured in the server with some &lt;code&gt;manage.py prepare_db_for_e2e&lt;/code&gt; script to add necessary data (user accounts, etc). And two, CI becomes more complex with server setup and teardown.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pytest-django.readthedocs.io/en/latest/index.html"&gt;pytest-django&lt;/a&gt; to the rescue! The library ships with a &lt;code&gt;live_server&lt;/code&gt; &lt;a href="https://pytest-django.readthedocs.io/en/latest/helpers.html#live-server"&gt;fixture&lt;/a&gt;, which "runs a live Django server in a background thread." In practice this means an e2e test running against the django server can be as simple as:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django.urls&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;reverse&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;playwright.sync_api&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;import&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;Page,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;expect&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;test_load_home_page&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(page:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;Page,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;live_server):&lt;/span&gt;
    &lt;span style="color: #7e8aa1"&gt;# Note navigating to live_server.url&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;page&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;goto(live_server&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;url&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;+&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;reverse(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;home&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;))&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;expect(page)&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;to_have_title(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;My Great Website&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Magnificent: no separate server to stand up/tear down, it's all handled automatically. Now, running my e2e tests is as simple as running &lt;code&gt;pytest&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Test data can be loaded through the same mechanisms as other pytest tests, supporting strong code reuse. For example, at work I use a mix of django json fixtures and factory boy - all re-usable in this testing approach. Need to test viewing an order in a given state? Create the order with a factory and navigate to its url. Easy peasy.&lt;/p&gt;
&lt;p&gt;Seriously. This setup rocks.&lt;/p&gt;
&lt;h2&gt;Challenges&lt;/h2&gt;
&lt;p&gt;Still, no software solution is ever perfect. This setup has a few challenges that are worth mentioning.&lt;/p&gt;
&lt;h3&gt;Shared global data fixtures&lt;/h3&gt;
&lt;p&gt;First, the &lt;code&gt;live_server&lt;/code&gt; fixture running the server in a separate thread means that per-test db transaction rollbacks are not possible. In practice this means that after each test using the fixture, every database table is truncated. If your tests depend on global data fixtures (e.g. provided in an &lt;code&gt;django_db_setup&lt;/code&gt; override), then you'll run into problems. To resolve this, I introduced a new command line flag to pytest to separate e2e from non-e2e test runs:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #7e8aa1"&gt;# in global conftest.py&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;pytest_addoption&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(parser):&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;parser&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;addoption(&lt;/span&gt;
        &lt;span style="color: #D5FF80"&gt;&amp;quot;--e2e&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;action&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;store_true&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;default&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=False&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;help&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Enable when running e2e tests&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;determine_django_db_setup_scope&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(fixture_name,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;config):&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    e2e tests use the live_server fixture, which will wipe&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    the db after each test run, which conflicts with a&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    session scoped db setup.&lt;/span&gt;

&lt;span style="color: #7e8aa1"&gt;    therefore, we use a function scoped db setup for e2e tests.&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;if&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;config&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;getoption(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;--e2e&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;):&lt;/span&gt;
        &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;function&amp;quot;&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;return&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;session&amp;quot;&lt;/span&gt;

&lt;span style="color: #7e8aa1; font-weight: bold; font-style: italic"&gt;@pytest&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;fixture(scope&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;determine_django_db_setup_scope)&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;django_db_setup&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(django_db_setup,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django_db_blocker):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I also separated e2e tests into their own top-level test folder, excluded by default in &lt;code&gt;pytest.ini&lt;/code&gt;. In practice this means running the full test suite requires two commands:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #7e8aa1"&gt;# run e2e tests&lt;/span&gt;
$&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;pytest&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;--e2e&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;tests_e2e/
&lt;span style="color: #7e8aa1"&gt;# run standard unit tests&lt;/span&gt;
$&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;pytest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ideally, this test classification would be unnecessary, but it's a minor setback.&lt;/p&gt;
&lt;h3&gt;Static assets&lt;/h3&gt;
&lt;p&gt;Another gotcha is configuring the static assets path, so that js, css and other static content is loaded from the test server correctly. To resolve this, I created a custom live server fixture:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #7e8aa1; font-weight: bold; font-style: italic"&gt;@pytest&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;fixture&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;my_live_server&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(live_server,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;settings):&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    Customized live_server fixture that configures STATIC_URL to point to the live server URL.&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;settings&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;STATIC_URL&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;live_server&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;url&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;+&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;/assets/&amp;quot;&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;yield&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;live_server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And then replaced the test parameters with &lt;code&gt;my_live_server&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Test teardown synchronization&lt;/h3&gt;
&lt;p&gt;The final issue that I've experienced in practice is a non-obvious test failure:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Bash&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Exception&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;Database&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;test_xxxxxxxx&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;couldn&lt;span style="color: #D5FF80"&gt;&amp;#39;t be flushed. Possible reasons:&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;  * The database isn&amp;#39;&lt;/span&gt;t&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;running&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;or&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;isn&lt;span style="color: #D5FF80"&gt;&amp;#39;t configured correctly.&lt;/span&gt;
&lt;span style="color: #D5FF80"&gt;  * At least one of the expected database tables doesn&amp;#39;&lt;/span&gt;t&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;exist.
&lt;span style="color: #d4d2c8"&gt;  &lt;/span&gt;*&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;The&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;SQL&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;was&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;invalid.
Hint:&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;Look&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;at&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;the&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;output&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;of&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;django-admin sqlflush&amp;#39;&lt;/span&gt;.&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;That&lt;span style="color: #D5FF80"&gt;&amp;#39;s the SQL this command wasn&amp;#39;&lt;/span&gt;t&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;able&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;to&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;run.
The&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;full&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;error:&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;deadlock&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;detected
DETAIL:&lt;span style="color: #d4d2c8"&gt;  &lt;/span&gt;Process&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;37253&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;waits&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;for&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;AccessExclusiveLock&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;on&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;relation&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;131803&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;of&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;database&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;131722&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;; &lt;/span&gt;blocked&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;by&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;process&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;37250&lt;/span&gt;.
Process&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;37250&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;waits&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;for&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;AccessShareLock&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;on&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;relation&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;132659&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;of&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;database&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;131722&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;; &lt;/span&gt;blocked&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;by&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;process&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;37253&lt;/span&gt;.
HINT:&lt;span style="color: #d4d2c8"&gt;  &lt;/span&gt;See&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;server&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;log&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;for&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;query&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;details.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It was discovered this is &lt;a href="https://github.com/pytest-dev/pytest-django/issues/454"&gt;due to asynchronous requests in the browser not finishing before the test ended&lt;/a&gt;. For example, consider a React-based page that fetches data asynchronously on load. If the playwright test merely checks the page's title before ending, then the test may end before the asynchronous data has been fetched.&lt;/p&gt;
&lt;p&gt;To resolve this issue, I typically add explicit waiting on the relevant network requests:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;with&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;page&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;expect_response(my_live_server&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;url&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;+&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;reverse(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;my_api&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ideally, pending network requests could be handled more gracefully.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Update Sep 2025&lt;/em&gt;: see &lt;a href="https://blog.tmk.name/2025/09/17/investigating-the-pytest-django-teardown-race-condition/"&gt;a new investigation and possible solution&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;p&gt;The challenges with this approach are greatly outweighed by the benefits. I have yet to find an e2e test setup that doesn't have issues. Writing tests in the same framework (pytest) and with the same tooling (data fixtures, mocks and so on) is a major boon compared to a different language, framework and tooling.&lt;/p&gt;
&lt;p&gt;I'll close out by saying that, almost a year after introducing playwright at work, few e2e tests have been written! It goes to show something very important about tests: the real challenge is team culture; no matter how easy it is to write tests there has to be buy-in from leadership and team for the importance and value of tests.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Two Years of Fellowship</title><link href="https://blog.tmk.name/2025/03/24/two-years-of-fellowship/" rel="alternate"></link><published>2025-03-24T00:00:00-04:00</published><updated>2025-03-24T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-03-24:/2025/03/24/two-years-of-fellowship/</id><summary type="html">&lt;p&gt;This is a follow-up to my &lt;a href="https://blog.tmk.name/2024/03/02/one-year-of-fellowship/"&gt;one year reflection post last year&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Changes&lt;/h2&gt;
&lt;p&gt;Recently, and more generally, I've experienced changes in my life, mainly in my relationships. Some of my early fellowship friendships have ended, some abruptly and others slowly. I am navigating changes within current relationships, some newly forming …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is a follow-up to my &lt;a href="https://blog.tmk.name/2024/03/02/one-year-of-fellowship/"&gt;one year reflection post last year&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Changes&lt;/h2&gt;
&lt;p&gt;Recently, and more generally, I've experienced changes in my life, mainly in my relationships. Some of my early fellowship friendships have ended, some abruptly and others slowly. I am navigating changes within current relationships, some newly forming, others transitioning. These changes are challenging for me, emotionally and spiritually. I feel more aware of my dysfunctional patterns of behavior, my desire to "act out" emotionally, throw my life into chaos, bury my head in the sand.&lt;/p&gt;
&lt;p&gt;The metaphor that's resonated with me recently is Rocky Balboa, going the distance with Apollo Creed. There's no victory in relationships, no knockout, no celebration. There is throwing in the towel, quitting, or getting knocked down and not getting up again.&lt;/p&gt;
&lt;p&gt;I feel like I've been going the distance. If I'm generous with myself, the rounds started long ago.&lt;/p&gt;
&lt;p&gt;I asked myself why I am experiencing challenges in my relationships. Wasn't my life calmer in the past? Ah, but it was so much poorer spiritually - I had so much less connection, so many fewer friends; naturally I struggled less.&lt;/p&gt;
&lt;p&gt;Relationships are the causes, products and ingredients of my recovery.&lt;/p&gt;
&lt;p&gt;Okay, that's enough getting current. I needed to get that out.&lt;/p&gt;
&lt;h2&gt;Staying the same&lt;/h2&gt;
&lt;p&gt;So much of my life has stayed the same in the past year. I live in the same apartment, drive the same car, have the same job. My relationship to what's stayed the same has changed, though. I feel more stable than ever at work, navigating the challenges of a changing role and long tenure. My apartment is feeling more like home. I'm addressing issues with my car prudently.&lt;/p&gt;
&lt;p&gt;I'm adopting a two pronged approach to serenity. One, I aim to work on acceptance of my current situation, whatever it may be. Two, I want to empower myself to make changes and take risks. These go hand in hand for me, as I cannot predict what the future will hold: perhaps I'll remain in the same job/apartment/car for the next five years, or perhaps my company will go bankrupt/car will explode/apartment burn down tomorrow. I want to be able to acknowledge and accept life, no matter the outcome.&lt;/p&gt;
&lt;h2&gt;Mountains beyond mountains&lt;/h2&gt;
&lt;p&gt;A key learning in recovery for me is that each stage reveals the next. And there is always a next. I had been feeling steady recently, having navigated emotional challenges in recent months (dating, travel, family). Sure enough, by remaining committed and invested in healing, I soon found more peaks hidden behind the clouds.&lt;/p&gt;
&lt;p&gt;While it's intimidating, and at times exhausting, it's the most rewarding and fruitful actions I've taken for myself. At Empty Cloud recently, the monk shared that becoming aware can be quite painful. After all, reality contains a lot of pain. As I lessen up on my emotional and spiritual painkillers, as I grow in my awareness, I am experiencing the pain. And I am coming out stronger for it. The monk said that it gets easier as equanimity grows.&lt;/p&gt;
&lt;h2&gt;Dating&lt;/h2&gt;
&lt;p&gt;Speaking of relationships, I would be remiss not to discuss how my dating strategy has changed in the past year. I joined a second fellowship that focuses on dating and intimacy. For me the programs fit together like puzzle pieces. The people that I've met there demonstrate powerful vulnerability and honesty, it's a wellspring of inspiration for me. I'm so glad that I am part of this community.&lt;/p&gt;
&lt;p&gt;My approach to dating is the healthiest it's ever been. I am finally slowing down enough to get to know someone before committing. I am setting boundaries with myself and others. I am developing confidence in myself. While I haven't been "successful" in the sense of finding a long term partner yet, I feel that I am no longer blindly stumbling from dysfunctional relationship to dysfunctional relationship. I am showing up authentically as myself, and letting others reveal their authentic selves.&lt;/p&gt;
&lt;h2&gt;Hope&lt;/h2&gt;
&lt;p&gt;Throughout all the challenging emotional struggles of the past year, the seed of hope has grown within me. I feel that I can handle what life throws at me. And when I can't, and need to fall apart, I know that I'll have my friends there to help me pick myself back up again.&lt;/p&gt;
&lt;p&gt;I am doing things that I never dreamed of (at times I don't even feel like myself!). I am reconnecting with my family, volunteering and making myself of service, and building healthy relationships with myself and others.&lt;/p&gt;
&lt;p&gt;I cannot wait to write my next update in a year.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Security Stories Part 2</title><link href="https://blog.tmk.name/2025/03/17/security-stories-part-2/" rel="alternate"></link><published>2025-03-17T00:00:00-04:00</published><updated>2025-03-17T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-03-17:/2025/03/17/security-stories-part-2/</id><summary type="html">&lt;p&gt;Security vulnerabilities are &lt;del&gt;rarely&lt;/del&gt; usually not as simple as a view executing a query param against the database. More often, they're comprised of multiple steps, forming a chain from crafted input to intermediary to - gotcha!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Boris" src="https://blog.tmk.name/images/goldeneye-boris.gif" /&gt;&lt;/p&gt;
&lt;p&gt;It takes thorough understanding of the code base and adopting the mindset of an attacker …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Security vulnerabilities are &lt;del&gt;rarely&lt;/del&gt; usually not as simple as a view executing a query param against the database. More often, they're comprised of multiple steps, forming a chain from crafted input to intermediary to - gotcha!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Boris" src="https://blog.tmk.name/images/goldeneye-boris.gif" /&gt;&lt;/p&gt;
&lt;p&gt;It takes thorough understanding of the code base and adopting the mindset of an attacker. Putting various subsystems together in unintended ways to yield disastrous consequences.&lt;/p&gt;
&lt;p&gt;Sometimes, it just takes a little curiosity - this can't &lt;em&gt;possibly&lt;/em&gt; work, right?&lt;/p&gt;
&lt;h2&gt;Whose MFA is it anyway?&lt;/h2&gt;
&lt;p&gt;At one company, we built support into the application for MFA - multi factor authentication - via SMS. When a user attempts to log in, they are authenticated, but have a flag in their session that MFA has not been validated, preventing them from taking any actions.&lt;/p&gt;
&lt;p&gt;All's well and good. Mostly. There's a itch behind my knee that I just can't manage to scratch. Session management is tricky to get right. Especially when developers write their own authentication logic, instead of using the platform or library defaults.. which was the case here.&lt;/p&gt;
&lt;p&gt;Looking at the authentication code for the tenth time, I asked myself if it were possible to set a value in a session and keep it after login. If I could set "mfa validated" to true in my session, and then login, I'd bypass MFA. How could I do that though?&lt;/p&gt;
&lt;p&gt;Ah, right. I could sign in with a separate account, for which I &lt;em&gt;can&lt;/em&gt; validate the MFA. If I then try to sign in on top of that, I could keep the session and bypass MFA for the second account.&lt;/p&gt;
&lt;p&gt;Can't possibly work, right? It does! The bug lied in the session regeneration code after login. Instead of generating a brand new session, the user inherited the existing session and was authenticated on top of it. Truly "multi" factor - use any additional factor 🤓&lt;/p&gt;
&lt;h2&gt;The Great Merge&lt;/h2&gt;
&lt;p&gt;At another company (what a long career 🫠), I went vulnerability hunting when I joined. I cannot believe that this works at every company. Seriously, if you want to shine before performance reviews, do a lookup for common escape hatch code (xss escaping disabled, csrf exempt view, etc.). Developers are lazy by nature and happily commit &lt;code&gt;dangerouslySetInnerHTML={untrustedInput}&lt;/code&gt; before rushing to clock out. The more pickins' for me.&lt;/p&gt;
&lt;p&gt;In my search I found several csrf exempt views. Most were fairly benign, uninteresting customer endpoints seemingly due to trouble with ajax requests. Again, rather than configure csrf protection correctly, developers chose to disable it. ¯\_(ツ)_/¯&lt;/p&gt;
&lt;p&gt;One view that stood out to is the internal tool to merge customers. You know, customer signs up with email address A and then forgets and signs up with email address B, and gets all mixed up so the customer support team merges the accounts to settle the issue.&lt;/p&gt;
&lt;p&gt;Now, missing csrf protection is not enough in 2025. By default, the major browsers will treat cookies as same site lax, which basically means "no cookie for you" for POST requests from other domains.&lt;/p&gt;
&lt;p&gt;Now, developers could not possibly have disabled same site cookie protection, right? Say, explicitly set same site "none" so that "yes take this cookie please"? Oh yes, they did. In order to handle some arcane single sign on flow, which used POST to provide auth creds, developers had disabled same site protection. Chain complete!&lt;/p&gt;
&lt;p&gt;Almost. Internal tools use uuids to refer to objects. So even with same site cookie and csrf protection disabled, I'd have to figure out the relevant uuids of customers to actually do anything. Did I really come this far to fail?&lt;/p&gt;
&lt;p&gt;Of course not, or I wouldn't be writing about this (though the cookie story is a good one in its own right). The customer merge tool uniquely does not accept uuids but integer ids. Cha-ching. I can easily merge customers together by guessing ids. Or incrementing ids..&lt;/p&gt;
&lt;p&gt;If I can count incremental ids, why don't I do that on repeat - merge customer 2 into 1, 3 into 1, 4 into 1, and so on: I could merge every single customer into a single super customer! 🚀&lt;/p&gt;
&lt;p&gt;I never did try this in practice (a sudden database restore would not have pleased the higher ups), but it remains a fun thought experiment.&lt;/p&gt;
&lt;h2&gt;More to come&lt;/h2&gt;
&lt;p&gt;I'm finding this quite cathartic. I derive pride and pleasure from finding and fixing vulnerabilities. I may dig a little deeper in the memory and write some more.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Security Stories Part 1</title><link href="https://blog.tmk.name/2025/03/02/security-stories-part-1/" rel="alternate"></link><published>2025-03-02T00:00:00-05:00</published><updated>2025-03-02T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-03-02:/2025/03/02/security-stories-part-1/</id><summary type="html">&lt;p&gt;I have a knack for finding security flaws in software applications. When I read about breaches in the news, I am not surprised, merely dismayed. Perhaps contributing to a more honest culture that shares openly about security flaws could help.&lt;/p&gt;
&lt;p&gt;In that vein, let me share some of the security …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have a knack for finding security flaws in software applications. When I read about breaches in the news, I am not surprised, merely dismayed. Perhaps contributing to a more honest culture that shares openly about security flaws could help.&lt;/p&gt;
&lt;p&gt;In that vein, let me share some of the security flaws that I've discovered at companies throughout my career. I've found quite a few, so this will be a series of posts.&lt;/p&gt;
&lt;h2&gt;The query endpoint&lt;/h2&gt;
&lt;p&gt;At one company I worked for, we built custom websites for small businesses. Some were storefronts for pizza parlors, others more involved applications. The founder of the company was a self taught programmer, better at business than programming. This was demonstrated in his code: get the job done and get paid.&lt;/p&gt;
&lt;p&gt;I introduced source code management at this company. I also introduced a ticketing system. I tried to mature the company's engineering practices. It was an uphill battle.&lt;/p&gt;
&lt;p&gt;Engineers understand the need to debug production systems. The bug can't be reproduced locally, despite trying every imaginable configuration of data states. So we add the functionality to impersonate a user's account, or hook up our local server with a readonly connection to the production database. Tools of the trade.&lt;/p&gt;
&lt;p&gt;My boss, clever little devil, considered these approaches too complex, too much effort. He added a web endpoint for querying the production database. It looked like this:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;PHP&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66; font-weight: bold"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;// Execute query directly (INSECURE!)&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;$result&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;$conn&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;-&amp;gt;&lt;/span&gt;&lt;span style="color: #FFD173"&gt;query&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;$_GET[&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;query&amp;#39;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;);&lt;/span&gt;

&lt;span style="color: #FFAD66"&gt;while&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;($row&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;$result&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;-&amp;gt;&lt;/span&gt;&lt;span style="color: #FFD173"&gt;fetch_assoc&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;())&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{&lt;/span&gt;
    &lt;span style="color: #FFD173"&gt;print_r&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;($row);&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;echo&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;&amp;lt;br&amp;gt;&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;;&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Behind a VPN, you ask? Protected with HTTP Basic Auth, you wonder as you start to sweat? Nope. What you see is what was live in production.&lt;/p&gt;
&lt;p&gt;Can't beat the convenience when debugging in production.&lt;/p&gt;
&lt;h2&gt;The password-less login&lt;/h2&gt;
&lt;p&gt;At another company, I worked a lot on the signup funnel. It was comprised of multiple steps, and we were always attempting to optimize conversion by making tweaks to such and such page, or rearranging the order of the steps. At a certain step in the funnel, the user enters their email address.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Login form" src="https://blog.tmk.name/images/email-funnel-step.png" /&gt;&lt;/p&gt;
&lt;p&gt;My usual approach to test accounts is the gmail &lt;code&gt;+&lt;/code&gt; trick. For those not in the know, gmail will accept emails with additional alphanumeric characters after a &lt;code&gt;+&lt;/code&gt; sign, for example if the base address is &lt;code&gt;luke.skywalker@gmail.com&lt;/code&gt;, then &lt;code&gt;luke.skywalker+1@gmail.com&lt;/code&gt; and &lt;code&gt;luke.skywalker+test001@gmail.com&lt;/code&gt; will correctly forward. I am in the habit of incrementing &lt;code&gt;+001&lt;/code&gt;, &lt;code&gt;+002&lt;/code&gt;, and so on.&lt;/p&gt;
&lt;p&gt;I so frequently made new accounts in the signup funnel that it became almost second-nature. Almost. One day, when testing some change in the funnel, I entered &lt;code&gt;test+042@gmail.com&lt;/code&gt;. Hmm. I had the sudden suspicion that I had made a mistake and re-used an existing test account. I had forgotten to increment.&lt;/p&gt;
&lt;p&gt;Then I noticed that I was logged in as &lt;code&gt;test+042@gmail.com&lt;/code&gt;. As in, fully authenticated.. without entering a password.. the hair on my neck started to rise!&lt;/p&gt;
&lt;p&gt;I quickly confirmed this by logging in as my boss in production. All I had to do was enter his email address in the signup funnel: &lt;code&gt;boss@company.com&lt;/code&gt;. I was so excited when I shared this with my boss, his reply, calm and collected: &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;nice find, when can you fix it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I learned an important lesson that day. Reporting an issue means that I'm likely to be the one to fix it. I'm such a professional that this doesn't prevent me from reporting, of course 🫠&lt;/p&gt;
&lt;h2&gt;More to come&lt;/h2&gt;
&lt;p&gt;I hope to write more about security. I think sharing some war stories is helpful for others to feel comfortable sharing. I hope it also dissolves the myth that security can be taken for granted.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Ride the Wave</title><link href="https://blog.tmk.name/2025/02/16/ride-the-wave/" rel="alternate"></link><published>2025-02-16T00:00:00-05:00</published><updated>2025-02-16T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-02-16:/2025/02/16/ride-the-wave/</id><summary type="html">&lt;p&gt;I posted this picture to a slack channel at work a few weeks ago:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Performance improvement over time" src="https://blog.tmk.name/images/performance-graph.png" /&gt;&lt;/p&gt;
&lt;p&gt;And I got attention (as in, a slack reaction) from several in the company, including the CEO. My boss, responding to or developing the momentum, suggested I talk about the effort in the next team presentation …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I posted this picture to a slack channel at work a few weeks ago:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Performance improvement over time" src="https://blog.tmk.name/images/performance-graph.png" /&gt;&lt;/p&gt;
&lt;p&gt;And I got attention (as in, a slack reaction) from several in the company, including the CEO. My boss, responding to or developing the momentum, suggested I talk about the effort in the next team presentation to management. I was surprised, as I post a lot on slack and rarely receive such a response.&lt;/p&gt;
&lt;p&gt;What is the image of? The time to load a key page on our website. In theory, an improvement of this magnitude should make a meaningful impact to the business bottom line. So how did I accomplish this sizeable feat?&lt;/p&gt;
&lt;h2&gt;Engineering initiatives&lt;/h2&gt;
&lt;p&gt;My company follows a fairly standard prioritization process. Product managers tend to a ticket queue, from which engineers pull to get their next task or project.&lt;/p&gt;
&lt;p&gt;What makes my company a little different is the autonomy given to engineers. We're encouraged to exercise our judgment to tickets on the queue. If the effort is under-estimated, and the payoff over-estimated, a ticket can be passed back to product managers for re-consideration.&lt;/p&gt;
&lt;p&gt;Engineers are also in charge of the more technical projects, think typical maintenance tasks like routine package upgrades. The lines get blurred at technical initiatives that may have a business payoff, but don't really fall into the realm of our product managers.&lt;/p&gt;
&lt;p&gt;One good example is site reliability and performance. The only times a product manager has expressed interest in these topics is when there's a problem, i.e. the website is crashing or so slow as to harm our operations.&lt;/p&gt;
&lt;h2&gt;Exploratory coding&lt;/h2&gt;
&lt;p&gt;I like to tinker and poke around in the code base. Usually, I'm on the hunt for security vulnerabilities. On this occasion I was investigating the website performance of a particular page. I became intrigued by the slow performance, and eagerly tore the code apart, looking for the culprit.&lt;/p&gt;
&lt;p&gt;This type of work is important and under-appreciated and under-discussed. Engineers need time to let their minds wander, to figure out how that one system works, to prove to themselves that bug fix from last year is still working, and to try to prove their coworker wrong about that argument in the pull request (hey, not all motivation is pure at heart!).&lt;/p&gt;
&lt;p&gt;I was able to diagnose an obvious performance regression on this page. The caching implementation was broken, in addition to unnecessary data processing. Basically, it took me an hour to fix, and an extra hour to double check my code.&lt;/p&gt;
&lt;h2&gt;Socializing wins&lt;/h2&gt;
&lt;p&gt;My fix was released at the end of the week. The next week I checked to see if it had made a meaningful impact. I should note that one coworker who tested the fix claimed that there was no performance improvement on his machine - a classic eye twitch generating comment!&lt;/p&gt;
&lt;p&gt;To verify the site performance, I had to jump through hoops to download production logs, and I even installed new software to generate the graph. I'll come back to this.&lt;/p&gt;
&lt;p&gt;As I gain maturity in my career, I am putting more effort into the &lt;em&gt;social&lt;/em&gt; aspect of my work. As an engineer, it's easy for me to respond to a problem by writing code and creating a pull request. It's &lt;em&gt;not&lt;/em&gt; easy for me to go through the trouble of proposing an idea to the team, building consensus, delegating, and then following through and verifying results. Much easier to create a pull request, even if it never does get merged..&lt;/p&gt;
&lt;p&gt;I took this small opportunity to post about the performance win to a public slack channel. I expected a small kudos from my boss and maybe my fellow engineers. Getting the CEO's eyes was welcome - and another opportunity!&lt;/p&gt;
&lt;h2&gt;Ride the Wave&lt;/h2&gt;
&lt;p&gt;My team has had its current logging setup for years, before I joined. Its functionality is limited, and it's not available to all engineers nor the product managers. To do anything remotely interesting requires going through its HTTP API. That is how I generated my graph above: I wrote a script to download the logs, convert them to a friendlier csv format, uploaded the csv to my local apache superset instance, and was then able to plot web metrics over time. Talk about barriers to entry!&lt;/p&gt;
&lt;p&gt;I believe in setting people up for success. When it's this challenging to analyze production performance metrics, engineers just aren't going to do it. To that end, I seized the opportunity of the CEO and my boss's interest in performance, and advocated for the integration of a superior logging setup. The new tooling would be available to everyone on the team and make it easy to explore and investigate site performance - in addition to other metrics logged. The cost is negligly higher than the current product. Win-win!&lt;/p&gt;
&lt;p&gt;I am pleased to say that work has begun on integrating the new logging tooling. I am proud of myself for identifying and seizing the opportunity to make this improvement for my team and the business. I did not anticipate working on logging tooling improvements when I was investigating a performance regression, nor when I was showcasing the results.&lt;/p&gt;
&lt;p&gt;When the wave comes, ride it and see where it takes you.&lt;/p&gt;</content><category term="career"></category></entry><entry><title>Reproduce Flaky Tests in pytest</title><link href="https://blog.tmk.name/2025/02/06/reproduce-flaky-tests-in-pytest/" rel="alternate"></link><published>2025-02-06T00:00:00-05:00</published><updated>2025-02-06T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-02-06:/2025/02/06/reproduce-flaky-tests-in-pytest/</id><summary type="html">&lt;p&gt;At work, I occasionally run into flaky tests in CI that prove troublesome to reproduce. Running the test by itself does not fail, nor does running all tests in the module. Sometimes it takes running the entire test suite to trigger the failure - a behemoth at work, currently taking about …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At work, I occasionally run into flaky tests in CI that prove troublesome to reproduce. Running the test by itself does not fail, nor does running all tests in the module. Sometimes it takes running the entire test suite to trigger the failure - a behemoth at work, currently taking about 10 minutes parallelized 🫠 Clearly this isn't a feasible approach to debugging the test.&lt;/p&gt;
&lt;p&gt;My theory for why these tests usually fail in the test suite but not in isolation is randomness, or more specifically, the randomness within the test data generation. We use &lt;code&gt;faker&lt;/code&gt; to generate fake data for our Django models. Importantly, we fix the faker seed at the start of the test run. For a given random seed, the flaky test may pass or fail.&lt;/p&gt;
&lt;p&gt;This has been borne out in practice. Usually the test is flaking for some specific random value. In one case, it was due to a uuid having a specific substring, &lt;code&gt;b2b&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Reproducing flaky tests&lt;/h2&gt;
&lt;p&gt;Rather than running the entire test suite, I came up with the idea of re-running the flaky test using a pytest parameter:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #7e8aa1; font-weight: bold; font-style: italic"&gt;@pytest&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;mark&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;parametrize(&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;iterations&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #FFD173"&gt;range&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;5&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;))&lt;/span&gt;
&lt;span style="color: #7e8aa1; font-weight: bold; font-style: italic"&gt;@pytest&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;mark&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;django_db&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;test_flaky_test&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(iterations):&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run just the flaky test with &lt;code&gt;pytest -k path.to.test_flaky_test&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Sometimes a low number of iterations like 5 suffices, occasionally 100 or even 1,000 iterations are necessary to flake. Once the test is reliably failing, debugging is straightforward, and feedback loops are much faster.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Cyteen</title><link href="https://blog.tmk.name/2025/01/18/cyteen/" rel="alternate"></link><published>2025-01-18T00:00:00-05:00</published><updated>2025-01-18T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-01-18:/2025/01/18/cyteen/</id><summary type="html">&lt;p&gt;This month I read &lt;em&gt;Cyteen&lt;/em&gt; by C.J. Cherryh for my sci fi book club.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 2 ½ stars.&lt;/p&gt;
&lt;h2&gt;Drinking game&lt;/h2&gt;
&lt;p&gt;Okay, drink every time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Justin is traumatized&lt;/li&gt;
&lt;li&gt;Gehenna or Sociogenesis come up&lt;/li&gt;
&lt;li&gt;Someone gets Worked&lt;/li&gt;
&lt;li&gt;Any capitalized Word&lt;/li&gt;
&lt;li&gt;Justin and Grant make coffee&lt;/li&gt;
&lt;li&gt;Someone …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;This month I read &lt;em&gt;Cyteen&lt;/em&gt; by C.J. Cherryh for my sci fi book club.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 2 ½ stars.&lt;/p&gt;
&lt;h2&gt;Drinking game&lt;/h2&gt;
&lt;p&gt;Okay, drink every time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Justin is traumatized&lt;/li&gt;
&lt;li&gt;Gehenna or Sociogenesis come up&lt;/li&gt;
&lt;li&gt;Someone gets Worked&lt;/li&gt;
&lt;li&gt;Any capitalized Word&lt;/li&gt;
&lt;li&gt;Justin and Grant make coffee&lt;/li&gt;
&lt;li&gt;Someone dines at &lt;em&gt;Changes&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Corain is stuck between the extremists and the expansionists&lt;/li&gt;
&lt;li&gt;Young Ari saves Reseune's finances (guppies, horses, etc.)&lt;/li&gt;
&lt;li&gt;Young Ari saves Reseune with a television interview&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Standby. Ari, this is Ari senior..&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I could go on. This book is a trip. It felt like more of a soap opera than a science fiction novel. There are so many plot holes and loose ends that I read the ending pure &lt;em&gt;azi&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Waiting for the plot&lt;/h2&gt;
&lt;p&gt;From the beginning, which introduced a few dozen characters, only for a tiny few to materialize into recurring involvement in the story. But what was the story? There was no discernible plot, time just seemed to move along, events occasionally interspersed between dozens of pages of the same inner dialogue repeated over and over. By the end of the book, I realized there was no climax, no plot curve, just a steady stream of dialogue, internal monologue, and observation.&lt;/p&gt;
&lt;p&gt;Okay, but that wasn't the point, right? The point was the character development. Justin is psychogenesis'd into becoming Ari 2's close teacher and research partner - he has the potential for genius, but it has to be brought out through vicious means. Jordan was too headstrong, independent, and in constant conflict with Reseune, so Ari manipulates young Justin's psychsets to mold him into her ideal.&lt;/p&gt;
&lt;p&gt;Ari 2's development felt like it was on rails. There were not any surprises or subverted expectations, which may have been part of the point - she &lt;em&gt;became&lt;/em&gt; Ari senior, just as cunning and manipulative. Something about that didn't fit right for me. It seemed that Ari 2 was becoming a slightly different person than Ari senior, which I felt strongly when Ari 2 was becoming aware of the manipulation / psychogenesis. I half expected Ari 2 to give up the fight and choose a different path - I think this would've been a more fitting character arc.&lt;/p&gt;
&lt;h2&gt;Azi&lt;/h2&gt;
&lt;p&gt;Another recurring theme is the integration of azi into CIT society. There are multiple levels, from genetic engineering to the individual psychology to the group sociology down through following generations (sociogenesis). I thought it an interesting reversal from humanoid robot to robotic human, and it posed challenging moral questions: are the azi human? Can azi consent? Are humans able to be manipulated to such an extent? I would have liked more exploration of this.&lt;/p&gt;
&lt;h2&gt;Loose ends&lt;/h2&gt;
&lt;p&gt;So why did Denys try to kill Ari 2? Did Jordan kill Ari 1? Probably not, so who did and why? How did 20 years go by in Union after Ari 1's passing, with practically nothing changing in the political landscape? Who are the rest of the Family running Reseune?&lt;/p&gt;
&lt;p&gt;I felt quite frustrated with the loose ends and plot holes - for such a long book, I expected a cleaner ending.&lt;/p&gt;
&lt;p&gt;That said, I did manage to finish the book. &lt;/p&gt;</content><category term="books"></category></entry><entry><title>Closed Systems</title><link href="https://blog.tmk.name/2025/01/10/closed-systems/" rel="alternate"></link><published>2025-01-10T00:00:00-05:00</published><updated>2025-01-10T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-01-10:/2025/01/10/closed-systems/</id><summary type="html">&lt;p&gt;I have an idea for an application that would keep track of my relationships - friends, colleagues, family, so on. Its main purpose is to help me stay on top of regular contact and interactions, something that's easy for me to fall behind on. Ideally, it would also help me to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have an idea for an application that would keep track of my relationships - friends, colleagues, family, so on. Its main purpose is to help me stay on top of regular contact and interactions, something that's easy for me to fall behind on. Ideally, it would also help me to develop and nurture positive and healthy relationships, while winding down and backing away from unhealthy ones. I called the idea an FRM, a play on CRM (swap friend for customer).&lt;/p&gt;
&lt;p&gt;As I thought about building this, I realized that I'd need to access my contacts and social interactions across platforms. After all, I talk to some friends on discord, others via sms, or whatsapp, and there's one group chat on signal. There's email, slack, google calendar, instagram, hinge.&lt;/p&gt;
&lt;p&gt;That's a lot of platforms, and mostly they are not friendly to third party developers. I find this to be a real shame. I should be able to interact with my data on any major platform in whichever way I see fit. Email, an open protocol, supports this - I could download my inbox and parse my emails. However, even sms, plain text messaging stored locally on my phone, is apparently &lt;a href="https://support.google.com/googleplay/android-developer/answer/10208820?hl=en"&gt;not trivial to scrape&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I believe these closed systems hurt developers and consumers. This is becoming more apparent in the age of LLMs, when my locked away data is used by the closed platforms to build their own models, used against or sold back to me.&lt;/p&gt;
&lt;p&gt;I'd love to have a database of all of my social interactions throughout my digital life. Pictures and videos. Train and interact with models on that data set. "Talk" to myself at various stages of my life, get perspective on various events and relationships in my life.&lt;/p&gt;
&lt;p&gt;For the foreseeable future, unfortunately, this is a pipe dream.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Django: Speed Up Tests Slight-lier with In-Memory Sessions</title><link href="https://blog.tmk.name/2025/01/04/django-speed-up-tests-slight-lier-with-in-memory-sessions/" rel="alternate"></link><published>2025-01-04T00:00:00-05:00</published><updated>2025-01-04T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2025-01-04:/2025/01/04/django-speed-up-tests-slight-lier-with-in-memory-sessions/</id><summary type="html">&lt;p&gt;&lt;a href="https://adamj.eu/tech/2024/09/18/django-test-speed-last-login/"&gt;An article by Adam Johnson&lt;/a&gt; recommended a small test speedup for Django unit tests that use the request test client. By bypassing the authentication backend and logging in the user directly with &lt;code&gt;client.force_login()&lt;/code&gt;, and also disabling the last login signal handler, a few database queries are saved per-test run …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://adamj.eu/tech/2024/09/18/django-test-speed-last-login/"&gt;An article by Adam Johnson&lt;/a&gt; recommended a small test speedup for Django unit tests that use the request test client. By bypassing the authentication backend and logging in the user directly with &lt;code&gt;client.force_login()&lt;/code&gt;, and also disabling the last login signal handler, a few database queries are saved per-test run.&lt;/p&gt;
&lt;p&gt;As I explored implementing this at work, I asked myself - could we skip the database entirely somehow? Does Django require a database for sessions?&lt;/p&gt;
&lt;p&gt;The answer is no, Django does not require a database for sessions. In fact, Django supports several different session engines, including the cache - and the cache can be configured to use local memory as the backend!&lt;/p&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://docs.djangoproject.com/en/5.1/topics/http/sessions/#using-cached-sessions"&gt;documentation for configuring cached based sessions&lt;/a&gt; states:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The local-memory cache backend doesn’t retain data long enough to be a good choice, and it’ll be faster to use file or database sessions directly instead of sending everything through the file or database cache backends. Additionally, the local-memory cache backend is NOT multi-process safe, therefore probably not a good choice for production environments.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is usually good and fine for an isolated test environment, but not in all scenarios. Multi-process test environments, such as running e2e tests with the pytest &lt;code&gt;live_server&lt;/code&gt; fixture, would not be supported by this approach, since the test process and the server process do not share memory. Code that expects and interacts with database backed sessions will also (obviously) not work.&lt;/p&gt;
&lt;h2&gt;Configuration&lt;/h2&gt;
&lt;p&gt;To configure in-memory sessions for Django unit tests, set the following in your settings:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;SESSION_ENGINE&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;django.contrib.sessions.backends.cache&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You may also want to configure the following, if you are adding a new cache entry for this purpose:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;SESSION_CACHE_ALIAS&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;test-session-storage&amp;quot;&lt;/span&gt;

&lt;span style="color: #d4d2c8"&gt;CACHES&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
    &lt;span style="color: #D5FF80"&gt;&amp;quot;test-session-storage&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;{&lt;/span&gt;
        &lt;span style="color: #D5FF80"&gt;&amp;quot;BACKEND&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;django.core.cache.backends.locmem.LocMemCache&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
        &lt;span style="color: #D5FF80"&gt;&amp;quot;LOCATION&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;test-session-storage&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
  &lt;span style="color: #d4d2c8"&gt;}&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Performance&lt;/h2&gt;
&lt;p&gt;The following test was used for calculating the number of queries in each scenario:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #7e8aa1; font-weight: bold; font-style: italic"&gt;@pytest&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;mark&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;django_db&lt;/span&gt;
&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;test_login_queries&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(django_assert_num_queries):&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;user&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;UserFactory(email&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;test@example.com&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;password&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;client&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;Client()&lt;/span&gt;

    &lt;span style="color: #FFAD66"&gt;with&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;django_assert_num_queries(&lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;0&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;):&lt;/span&gt;

        &lt;span style="color: #d4d2c8"&gt;client&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;login(username&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;user&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;username,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;password&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

        &lt;span style="color: #FFAD66"&gt;assert&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;True&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #D5FF80"&gt;&amp;quot;This is a dummy test to test the number of queries.&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Without Adam's changes (i.e. no signal disconnection, and using &lt;code&gt;client.login()&lt;/code&gt;), the result is &lt;strong&gt;20 queries&lt;/strong&gt;. The base is fairly high due to a custom signal handler for user login.&lt;/p&gt;
&lt;p&gt;With Adam's changes, the result is &lt;strong&gt;14 queries&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With the local memory session backend, the result is &lt;strong&gt;0 queries&lt;/strong&gt;. Amazing!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;While stimulating as an exercise, in practice with my work codebase and test suite I found negiglible time savings. The test suite includes maybe a dozen tests using the test client, while the vast majority test at the service layer.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Thailand</title><link href="https://blog.tmk.name/2024/12/30/thailand/" rel="alternate"></link><published>2024-12-30T00:00:00-05:00</published><updated>2024-12-30T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-12-30:/2024/12/30/thailand/</id><summary type="html">&lt;p&gt;&lt;img alt="sea turtle energy" src="https://blog.tmk.name/images/thailand/1.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I spent 3 and a half weeks in Thailand in the month of November. This was a spectacular trip.&lt;/p&gt;
&lt;h2&gt;Trip report&lt;/h2&gt;
&lt;p&gt;I left on the day after the election, flying through Istanbul to Bangkok, 2 roughly 10 hour flights back to back. I arrived in the morning, waited for my …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="sea turtle energy" src="https://blog.tmk.name/images/thailand/1.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I spent 3 and a half weeks in Thailand in the month of November. This was a spectacular trip.&lt;/p&gt;
&lt;h2&gt;Trip report&lt;/h2&gt;
&lt;p&gt;I left on the day after the election, flying through Istanbul to Bangkok, 2 roughly 10 hour flights back to back. I arrived in the morning, waited for my friend at the airport, and then taxi'd to Khao San Road, my first stay.&lt;/p&gt;
&lt;p&gt;I have never been anywhere like Khao San before. Walking the street, I had all senses assaulted: sight, smell, sound. The music blared so loudly that I felt the air around me vibrating. Street sellers hawked their wares aggressively; eye-contact is to be avoided at all costs. The street is a carnival of sin.&lt;/p&gt;
&lt;p&gt;I spent a few days in Bangkok, exploring temples, museums, and wandering the city. Ending a day on my feet with a foot massage was heavenly.&lt;/p&gt;
&lt;p&gt;From Bangkok I traveled with my friend to Phuket. We stayed in Kata Beach, in a hotel primarily occupied by Russian families. Hearing spoken English made me giddy. This was my first time in a tropical island (though Phuket doesn't &lt;em&gt;feel&lt;/em&gt; like much of an island), and I made the most of it, relaxing on the beach and watching the sunset.&lt;/p&gt;
&lt;p&gt;We took a couple day trips to Phang Nga Bay, one snorkeling and swimming, and another kayaking. While heavily touristed and crowded, nothing could detract from the natural beauty of the bay and the islands. Snorkeling I saw tropical fish swim in the coral reefs (sadly, quite bleached). On the kayaking trip we explored caves and lagoons, finishing with a visit to the bioluminescent plankton. There was a thunderstorm in the distance, and the night sky was clear, so I saw above lightning and stars and below the glowing plankton.&lt;/p&gt;
&lt;p&gt;I think I achieved a form of adrenal burnout, I had so many amazing experiences, I could barely work up the excitement to leave the hotel by the end of the trip.&lt;/p&gt;
&lt;p&gt;After Phuket, I flew solo to Chiang Mai in the north. This was my first truly solo foreign trip. I spent almost a week on my own, hiking, day tripping, tasting delicious local coffee, and visiting an elephant sanctuary. Old town Chiang Mai was one of my favorite places I've visited, it felt dense yet small, busy but not crowded, and with fantastic food and coffee within a 15 minute walk.&lt;/p&gt;
&lt;p&gt;Finally I returned to Bangkok to close out the trip for a few days before flying home. I shopped for souvenirs and had one last lunch with my friend. &lt;/p&gt;
&lt;h2&gt;Recommendation&lt;/h2&gt;
&lt;p&gt;I recommend Thailand to everyone. It is such a friendly destination, easily accessible, easy to get around. There is something for everyone, for any taste, from bustling Asian city to tropical island to hiking in mountain jungles.&lt;/p&gt;</content><category term="travel"></category></entry><entry><title>End of Year Review (2024)</title><link href="https://blog.tmk.name/2024/12/23/end-of-year-review-2024/" rel="alternate"></link><published>2024-12-23T00:00:00-05:00</published><updated>2024-12-23T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-12-23:/2024/12/23/end-of-year-review-2024/</id><summary type="html">&lt;p&gt;It's once again the end of the year, and a time for reflection once again. This follows &lt;a href="https://blog.tmk.name/2023/12/23/end-of-year-review/"&gt;my end of year review for 2023&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Travel&lt;/h2&gt;
&lt;p&gt;I traveled to Amsterdam and Brussels in January/February. This was a bit of a chaotic trip, only about a week, and I was working …&lt;/p&gt;</summary><content type="html">&lt;p&gt;It's once again the end of the year, and a time for reflection once again. This follows &lt;a href="https://blog.tmk.name/2023/12/23/end-of-year-review/"&gt;my end of year review for 2023&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Travel&lt;/h2&gt;
&lt;p&gt;I traveled to Amsterdam and Brussels in January/February. This was a bit of a chaotic trip, only about a week, and I was working remotely part time. Amsterdam is a cute European city, with lovely scenery (architecture, canals) - it felt like a great destination to take your romantic partner. In Brussels I attended FOSDEM, my first tech conference. I was pretty overwhelmed with the bustle of the crowds. I found the city to be significantly more grimier and grittier than Amsterdam, though enjoyable and charming in its own way.&lt;/p&gt;
&lt;p&gt;In May, my friend/colleague visited the United States for the first time, and we traveled for a couple weeks together. First to Pittsburgh for PyCon, where we joined two other colleagues. While FOSDEM is volunteer run, PyCon has a strong organization backing it, and the difference was clear. PyCon had massive stages in a giant conference center. I found Pittsburgh to be charming, the downtown nestled in between rivers and mountains. After PyCon, my friend and I visited DC, joined by a third friend. That was a blast, I love DC, its small buildings, wide streets, and green spaces encourage me. Back in the greater New York area, we did some NYC tourism.&lt;/p&gt;
&lt;p&gt;In July, I took my first solo trip to Gloucester, Mass. It was a quick weekend getaway, a desperate attempt to catch some relaxation and escape the FOMO of others' travel plans. I went whale watching and sat on the beach for a rare Atlantic coast sunset.&lt;/p&gt;
&lt;p&gt;In August, I took two trips: first a weekend camping trip, then a trip to visit my brother's family in LA. This was my first time camping, and I had a stressful but enjoyable adventure with friends. In LA I got to visit my young nephew, who is just too adorable.&lt;/p&gt;
&lt;p&gt;What a year! In October I traveled to Chicago for my work's yearly management offsite. I enjoyed spending in person time with my team. On the last day, I went on the architecture boat tour, a really unique experience.&lt;/p&gt;
&lt;p&gt;I can't summarize my experiences in a paragraph - in November, I went to Thailand for 3 weeks. This was an incredible and life changing experience in so many ways. I will write more about this trip later. For here, I'll just say that this was the most amazing trip I've taken to date.&lt;/p&gt;
&lt;h2&gt;Books&lt;/h2&gt;
&lt;p&gt;I read 15 books this year, a decrease from the last. Mainly it was my sci fi reading group that kept me consistently reading throughout the year - I usually tend to read in bursts. My top books from this year:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.tmk.name/2024/05/18/the-saint-of-bright-doors/"&gt;The Saint of Bright Doors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tmk.name/2024/06/22/the-information/"&gt;The Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use of Weapons&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.tmk.name/2024/07/13/lathe-of-heaven/"&gt;The Lathe of Heaven&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The Empty Mirror&lt;/li&gt;
&lt;li&gt;In Ascension&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I want to note that this was my first foray into writing reviews for the books that I read. It made for a good series of blog posts, and joined nicely with the book club discussions.&lt;/p&gt;
&lt;h2&gt;Writing&lt;/h2&gt;
&lt;p&gt;I blogged once a week for 7 months, writing more than 30 articles. Wow! That's more than I've ever written before. It has been a learning experience, and a process of trust and letting go. I had set the goal of writing every week, and when I faltered, I wasn't able to pick back up on the momentum - the overdue backlog prevented me from just writing something.&lt;/p&gt;
&lt;p&gt;For next year, I want to write, but I will reduce my goal to twice per month. That will reduce the pressure - I would have accomplished that goal this year, actually. If I write more, great.&lt;/p&gt;
&lt;h2&gt;Career&lt;/h2&gt;
&lt;p&gt;I continued my job at Rinse. I worked on a number of projects, notably logistics efficiency, and improving system security. I was also assigned two direct reports, my first foray into people management. I have mentored and coached my reports for the past 6 months, culminating in delivering their end of year performance reviews. This has been a rewarding experience, and I'd like to write more about it.&lt;/p&gt;
&lt;p&gt;I attended my first two tech conferences this year. Having never attended before, and attending with other conference newbies, I felt fairly overwhelmed: the crowds, the networking, the events. Working remotely it was a lot to lean into suddenly, though I feel much better prepared for the next conference (PyCon '25?).&lt;/p&gt;
&lt;h2&gt;Projects&lt;/h2&gt;
&lt;p&gt;I did not work on side projects this year. I started on several, but never got far past the idea phase.&lt;/p&gt;
&lt;h2&gt;Personal development&lt;/h2&gt;
&lt;p&gt;Personal development was my main focus for 2024. Reflecting on my 20s, I see how my mental health was my biggest roadblock to success and happiness. In the past year, I regularly attended support groups, and built a support network for the first time in my life. I've met the kindest, most caring and loving people, and nurtured positive, healing friendships with them. I am truly grateful for their support.&lt;/p&gt;
&lt;p&gt;I continued attending meditation at my local Buddhist monastery. I sat for my longest meditation, of more than 50 minutes without break. I learned the dharma and started integrating myself into the sangha. The rewards of my meditation practice are stronger emotional resilience, presence and serenity.&lt;/p&gt;
&lt;p&gt;I took a break from dating for the first six months of the year, a challenging but immensely rewarding experience. I went on several first dates, and dated two women for 1-2 months each, culminating in a devastating breakup in December. I feel way more confident about dating and relationships than at any point before, having really put in the work on myself. I am excited to put myself out there in 2025.&lt;/p&gt;
&lt;p&gt;My experiences with travel this year were wonderful. I have a personal theory, something I haven't found the right words to summarize, that regular vacations and breaks (every 3-6 months) are a crucial step in development. The day to day grind, meditation, journaling, therapy, etc., builds and builds but it's when I take a step out of my normal life that suddenly all of the gains are realized - how about, like slowly revving the engine and then shifting into a higher gear.&lt;/p&gt;
&lt;h2&gt;Last year's goals&lt;/h2&gt;
&lt;p&gt;Looking at my goals from last year, let's see how I did:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🛑 Regular physical activity&lt;/li&gt;
&lt;li&gt;🛑 Reducing screen time&lt;/li&gt;
&lt;li&gt;🛑 Learning about finances and investing&lt;/li&gt;
&lt;li&gt;✅ Continuing to nurture my physical health&lt;/li&gt;
&lt;li&gt;✅ Continuing to nurture my mental health&lt;/li&gt;
&lt;li&gt;✅ Continuing to nurture my spirituality&lt;/li&gt;
&lt;li&gt;✅ Continuing to invest in friendships&lt;/li&gt;
&lt;li&gt;✅ Continuing to practice meditation&lt;/li&gt;
&lt;li&gt;✅ Continuing to eat well&lt;/li&gt;
&lt;li&gt;✅ Continuing to sleep well&lt;/li&gt;
&lt;li&gt;✅ FOSDEM and PyCon&lt;/li&gt;
&lt;li&gt;✅ International trip&lt;/li&gt;
&lt;li&gt;✅ Domestic trip&lt;/li&gt;
&lt;li&gt;🛑 Move abroad&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I struggled to engage in regular physical activity. Reflecting on that, I would like to try to socialize exercise, rather than go it alone - a key learning of mine in mental health that I want to deploy to other areas of my life.&lt;/p&gt;
&lt;p&gt;Same for reducing screen time. To be fair, I didn't really play video games at all last year, and that's coming from someone who was a big gamer only a couple years ago. So there's a clear shift in how I engage with technology. Notably, I watched a lot more tv and movies. I also radically changed my internet browsing habits, reducing to nigh zero some really unhealthy content intake. So while the specific goal wasn't met, I would be remiss not to recognize what I did achieve.&lt;/p&gt;
&lt;p&gt;Finances and investing - I've left this on autopilot, investing in basic index funds.&lt;/p&gt;
&lt;p&gt;Moving abroad, when I turned 30 in a panic I started Spanish lessons with the plan of moving to Barcelona for 3 months or so. I did not follow through on that plan, which I do not feel bad about. Someone wise said to me, be careful that you're running to something, not from something. And I was clearly running away from my emotional pain. I am glad I decided to hunker down in New Jersey, where I have the support to heal.&lt;/p&gt;
&lt;h2&gt;Next year's goals&lt;/h2&gt;
&lt;p&gt;Now, what about next year. It's easy for me to load up on goals, aim for the moon in a million different directions. Instead, I'll choose a few priorities.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Physical health&lt;ul&gt;
&lt;li&gt;Try joining a group fitness activity (club, class, studio, casual sport, etc)&lt;/li&gt;
&lt;li&gt;Trial a physical trainer&lt;/li&gt;
&lt;li&gt;Exercise 2x/wk&lt;/li&gt;
&lt;li&gt;Follow up on outstanding physical ailments&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Career&lt;ul&gt;
&lt;li&gt;Blog 2x/mth  (including technical/career content)&lt;/li&gt;
&lt;li&gt;Attend a tech conference (stretch - present a lightning talk / submit a talk proposal)&lt;/li&gt;
&lt;li&gt;Launch a side project (stretch - make my first independent $)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Relationships&lt;ul&gt;
&lt;li&gt;Nurture and develop friendships&lt;/li&gt;
&lt;li&gt;Attend in-person dating events&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Travel&lt;ul&gt;
&lt;li&gt;2 international trips (1 wk and 2 wks, maybe Morocco, Japan)&lt;/li&gt;
&lt;li&gt;3 domestic trips (camping, meditation retreat, ?)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The best year of my life&lt;/h2&gt;
&lt;p&gt;This has been a challenging year for me. I've experienced enormous pain and discomfort as I've worked on healing my emotional wounds. I no longer feel so alone, having found and nurtured a support network of loving friends. In so many ways I feel like the same person as a year ago, and yet I see the changes, the light pouring through the cracks. This past weekend I was visited by my ghosts of Christmas past. I am breaking patterns that were previously invisible. Every moment is a rebirth.&lt;/p&gt;
&lt;p&gt;Even though this month was emotionally devastating for me, I am standing tall, weathering the storm. As in small, so in the large. This year I stood tall and weathered the storms. I lived the fullest, most intentional, most fearless, most present year of my life.&lt;/p&gt;
&lt;p&gt;All of the effort in personal development is an investment into my present and my future. After my 2&lt;sup&gt;nd&lt;/sup&gt; best year of my life in a row, I can't wait for next year - no pressure on myself :)&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Lathe of Heaven</title><link href="https://blog.tmk.name/2024/07/13/lathe-of-heaven/" rel="alternate"></link><published>2024-07-13T00:00:00-04:00</published><updated>2024-07-13T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-07-13:/2024/07/13/lathe-of-heaven/</id><summary type="html">&lt;p&gt;The 2002 TV movie adaptation of &lt;em&gt;The Lathe of Heaven&lt;/em&gt; disappointed me. I liked that the bell hop seemed to remember George throughout the changes, but the overall plot was quite dry. So it was my surprise that the Wikipedia article mentions the adaptation excluded an alien invasion, plus all …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The 2002 TV movie adaptation of &lt;em&gt;The Lathe of Heaven&lt;/em&gt; disappointed me. I liked that the bell hop seemed to remember George throughout the changes, but the overall plot was quite dry. So it was my surprise that the Wikipedia article mentions the adaptation excluded an alien invasion, plus all the philosophical nuance. What in the world did I watch?&lt;/p&gt;
&lt;p&gt;My sci fi book club affirmed my suspicions. So I ordered the book to find out for myself.&lt;/p&gt;
&lt;p&gt;Let me just say - I was shocked. This book is incredible. Reality-bending, philosophical, psychological - a perfect complement to Philip K Dick, without the paranoia and terror.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book a rare 5 stars.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;George Orr is abusing drugs alternately to stop dreaming or to stay awake to avoid sleep. Because Orr's dreams have a tendency of creeping into the real world. His first memory of changing the world is when his aunt came to stay with his family. Orr didn't care for his aunt; he dreamed that the aunt had died in a car crash. Lo and behold he wakes up to find his aunt &lt;em&gt;had actually died some months ago in a car crash&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;As a sane person, this disturbed Orr. No one else remembered the previous reality. The guilt, the pressure, the responsibility weighed on him. So he abused drugs to stop dreaming.&lt;/p&gt;
&lt;p&gt;Drug abuse leads Orr to a state mandated therapist, a sleep and dream specialist, Dr. Haber. With the use of Haber's experimental new dream machine, the augmentor, Orr is given hypnotic suggestions and coaxed into dreaming. Dr. Haber comes to suspect and realize that Orr isn't just a psychotic quack - Haber, being so close to the dream epicenter, watches the world around him shift.&lt;/p&gt;
&lt;p&gt;The characters make this story as much as the plot. Haber is an idealist, an inventor, a scientist, a psychologist. He wants to tinker to try to change, to try to improve the world. He is a large man, both physically and psychically. Orr is the opposite, small, mild mannered, meek, inward and not outward focused. Orr works a simple job as a draftsman for the state, lives a modest and isolated life.&lt;/p&gt;
&lt;p&gt;Haber tries to use Orr's dreams to make the world a "better place." However dreams are not scientific, hypnotic suggestions are interpreted creatively by the subconscious. In an attempt to solve overpopulation, Orr dreams of a pandemic that decimates the world population. To achieve world peace, Orr dreams of an alien invasion that unites the warring nations.&lt;/p&gt;
&lt;p&gt;Orr becomes increasingly disturbed by Haber, though he doesn't quit the sessions. Orr reminds me of the protagonist from &lt;em&gt;Sheltered Lives&lt;/em&gt; - unable to act, to make a choice, trapped by himself in others' plans and designs. Orr does reach out to a lawyer, a woman named Lelache, to try to help him. Lelache, in attending a dream session, is thus caught up in the shifting realities, caught in Orr's wake.&lt;/p&gt;
&lt;p&gt;The story comes to a climax when Orr finally refuses to continue working with Haber. However, Haber has finally perfected the augmentor to allow himself to dream these reality-altering dreams. In dreaming, Haber starts to unravel the world, unable to form a coherent narrative, destroying rather than creating a new world.&lt;/p&gt;
&lt;p&gt;Orr reaches Haber and shuts off the machine. The world remains in shambles, contradictory realities mixed together. The story ends with Orr visiting Haber, now institutionalized.&lt;/p&gt;
&lt;p&gt;Oh and Orr makes friends with the aliens who he dreamed to become friendly with humans.&lt;/p&gt;
&lt;h2&gt;What I liked&lt;/h2&gt;
&lt;p&gt;What did I not like. This story suits my fancy in every aspect. I love the dichotomy between Orr and Haber. Orr is an excellent character, not a hero, not an anti-hero, but completely relate-able, suffering from the weight of global responsibility.&lt;/p&gt;
&lt;p&gt;I love love love reality bending stories - though that market was cornered by PKD, entries like &lt;em&gt;Dream Master&lt;/em&gt; and &lt;em&gt;The Lathe of Heaven&lt;/em&gt; rightfully claim their spot alongside &lt;em&gt;The Three Stigmata of Palmer Eldritch&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The plot was executed so well. Orr is buddha-like when he shuts off the augmentor, at peace yet projecting his will to shape not the world but his place within it.&lt;/p&gt;
&lt;h2&gt;What I didn't like&lt;/h2&gt;
&lt;p&gt;Maybe I longed for Orr and Lelache to end up together - though it's hinted, so not truly missing. Honestly I can't find any significant faults with this book. The fact that it's short is a bonus: fitting a story to its proper length is no small feat and makes a huge impact.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;Read the book, spend the next few weeks wondering whether your dreams are also changing the world while you sleep.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>Shifting Baselines</title><link href="https://blog.tmk.name/2024/07/13/shifting-baselines/" rel="alternate"></link><published>2024-07-13T00:00:00-04:00</published><updated>2024-07-13T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-07-13:/2024/07/13/shifting-baselines/</id><summary type="html">&lt;p&gt;I was reading an article about marine population collapse and in it was mentioned a concept called the "shifting baseline."&lt;/p&gt;
&lt;h2&gt;Implicit biases&lt;/h2&gt;
&lt;p&gt;The shifting baseline is a bias whereby what's &lt;em&gt;normal&lt;/em&gt; (that is, their baseline) is relative to one's perspective. Okay, sounds obvious enough - normal to me depends on what's …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I was reading an article about marine population collapse and in it was mentioned a concept called the "shifting baseline."&lt;/p&gt;
&lt;h2&gt;Implicit biases&lt;/h2&gt;
&lt;p&gt;The shifting baseline is a bias whereby what's &lt;em&gt;normal&lt;/em&gt; (that is, their baseline) is relative to one's perspective. Okay, sounds obvious enough - normal to me depends on what's normal to me. What's interesting about that?&lt;/p&gt;
&lt;p&gt;Consider what happens over time. In the marine population article, it describes how the first generation of marine scientists noticed that populations fell 80% in their lifetimes. Later generations noticed the same rate of collapse within their lifetimes. That is, each generation of scientists were introduced to a different baseline.&lt;/p&gt;
&lt;p&gt;So from 80% collapse to 20% remaining. From 20% remaining to 4%. From 4% to 0.8%. When each generation of scientists considers the population levels during their early careers as the baseline, they each notice only a modest relative decline, but over time this amounts to an exponential, catastrophic collapse.&lt;/p&gt;
&lt;h2&gt;Life&lt;/h2&gt;
&lt;p&gt;There are quantitative ways to demonstrate this bias in normal life. For example, inflation - ask your parents what they paid for a mortgage or to rent. Even within one's own life, the prices of some goods or services will rise or fall, such that it takes active effort to keep up to date with everyday prices.&lt;/p&gt;
&lt;p&gt;One area I have been applying the shifting baseline model to is overload and burn out. I was shocked by a study cited in &lt;em&gt;The Information&lt;/em&gt; in which participants elected to continue receiving more information despite it overloading them.&lt;/p&gt;
&lt;p&gt;I notice this tendency within myself. Taking on one more obligation, one more responsibility, signing up for one more activity, all seems innocuous - the incremental effort or load is minor. Over time, however, without monitoring my overall commitments, these all add up and threaten to collapse the entire house of cards.&lt;/p&gt;
&lt;p&gt;Along these lines, I can also grow accustomed to a certain level of responsibility, shifting my baseline to more and more activities. But I can't keep adding more - I have finite time, energy, availability. Something has to give. The patterns of the past can't keep accumulating.&lt;/p&gt;
&lt;h2&gt;Look around you&lt;/h2&gt;
&lt;p&gt;History is the antidote to the bias. Zoom out, try to see the big picture. Confused about current events? Don't understand why the world is the way it is? Don't understand how you got where you are? Look at the past, try to understand it to better understand the present. Don't get caught off guard by a shifting baseline!&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Witch King</title><link href="https://blog.tmk.name/2024/07/06/witch-king/" rel="alternate"></link><published>2024-07-06T00:00:00-04:00</published><updated>2024-07-06T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-07-06:/2024/07/06/witch-king/</id><summary type="html">&lt;p&gt;I just finished reading &lt;em&gt;Witch King&lt;/em&gt; by Martha Wells for my sci fi book club.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 2 stars.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;I am writing this some time after reading, so my memory is fuzzy.&lt;/p&gt;
&lt;p&gt;There are two plots in parallel across two periods of time. In one …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I just finished reading &lt;em&gt;Witch King&lt;/em&gt; by Martha Wells for my sci fi book club.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 2 stars.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;I am writing this some time after reading, so my memory is fuzzy.&lt;/p&gt;
&lt;p&gt;There are two plots in parallel across two periods of time. In one, in the past, we learn the history of the beginning of the rebellion against the Hierarchs. In the other, in the current, we follow the witch king Kai as he unravels the mystery of his imprisonment.&lt;/p&gt;
&lt;p&gt;Some time ago the Hierarchs invaded the continent and destroyed everybody in their path. Literally - they razed cities, slaughtered the nomadic tribes, using a powerful form of death magic. Why did the Hierarchs invade? We never find out.&lt;/p&gt;
&lt;p&gt;What we do find out is how Kai and a prince team up to break out of the palace where they are imprisoned. There's a strong degree of plot armor over the participants, as we follow their parallel journey in the future. Long story short, Kai manages to defeat the Hierarchs in the palace and the prisoners escape.&lt;/p&gt;
&lt;p&gt;In the present, Kai wakes to find himself imprisoned once again in an underwater tomb - water disables Kai's demon powers. After escaping, Kai joins with his band of friends to find their missing friend, in the hopes of figuring out how they were imprisoned.&lt;/p&gt;
&lt;p&gt;The band of heroes travels around the world, ultimately arriving back at the same palace from the earlier timeline. There the final showdown occurs: we discover the conspiracy consolidate power in the alliance that emerged after the defeat of the Hierarchs.&lt;/p&gt;
&lt;h2&gt;What I liked&lt;/h2&gt;
&lt;p&gt;I found this book an easy read, which was nice after slugging through &lt;em&gt;The Information&lt;/em&gt;, a far more challenging book. That said, I liked the different magic systems, and I thought there was potential in some of the characters, but I didn't feel it was executed upon.&lt;/p&gt;
&lt;h2&gt;What I didn't like&lt;/h2&gt;
&lt;p&gt;Clothes. Everybody's clothes are described in detail! Why.&lt;/p&gt;
&lt;p&gt;Who were the Hierarchs? What was their motivation to invade? What was their grand plan? So many unanswered questions.&lt;/p&gt;
&lt;p&gt;I also didn't care for Kai's lack of confidence. As a demon among humans, capable of draining the life from victims, and later learning how to conjure magic, I was disappointed that Kai was so awkward. I mean he's the Witch King! Act like it.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;I wouldn't recommend this book. There's more rewarding fantasy out there.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>Feedback Loops</title><link href="https://blog.tmk.name/2024/06/29/feedback-loops/" rel="alternate"></link><published>2024-06-29T00:00:00-04:00</published><updated>2024-06-29T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-06-29:/2024/06/29/feedback-loops/</id><summary type="html">&lt;p&gt;I've been reflecting on feedback loops since reading &lt;em&gt;The Information&lt;/em&gt;. Coming to work on a Monday morning, with fresh eyes, I jotted down a few factors affecting feedback loops in my day to day life.&lt;/p&gt;
&lt;h2&gt;Tickets&lt;/h2&gt;
&lt;p&gt;As an engineer, I expect the tickets in the queue to be appropriately prioritized …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've been reflecting on feedback loops since reading &lt;em&gt;The Information&lt;/em&gt;. Coming to work on a Monday morning, with fresh eyes, I jotted down a few factors affecting feedback loops in my day to day life.&lt;/p&gt;
&lt;h2&gt;Tickets&lt;/h2&gt;
&lt;p&gt;As an engineer, I expect the tickets in the queue to be appropriately prioritized, requirements explicit and clear, and reasonably scoped.&lt;/p&gt;
&lt;p&gt;When tickets don't meet these criteria, there's friction - sometimes small, sometimes large. Perhaps this ticket is no longer relevant. Perhaps this ticket is a vague bug report without reproduction steps ("XYZ is broken"). Or perhaps the queue is full of multi month projects.&lt;/p&gt;
&lt;p&gt;Over time, that friction becomes internalized. When I have time (waiting for code review, in between projects, etc.), I hesitate before looking into the queue. I prefer to work on developer initiatives, where I can control scope, understand requirements, and prioritize myself.&lt;/p&gt;
&lt;h2&gt;Time from code to release&lt;/h2&gt;
&lt;p&gt;Maintaining context for tickets is a significant challenge in software engineering. When I try to work on multiple tasks simultaneously, alongside overseeing others' tasks, performing code review, and planning future projects, I struggle to context switch multiple times a day and dozens of times a week.&lt;/p&gt;
&lt;p&gt;A long turnaround time between writing and releasing code implies a lengthy period of juggling multiple contexts. I am reluctant to pick up additional tickets while several are in code review, QA or pending release.&lt;/p&gt;
&lt;p&gt;Momentum and motivation fade quickly, and I've noticed lack of interest in monitoring and bug fixing from engineering as well as product when tickets are deployed weeks or months after ideation. The sentiment is "the excitement is gone, so let's move on."&lt;/p&gt;
&lt;p&gt;Yet another aspect is decreased hustle. If code I write today will take days to be reviewed, then another week to be tested, and yet another week to release - then there's little difference between finishing my code today or tomorrow or even later in the week.&lt;/p&gt;
&lt;h2&gt;Shifting winds&lt;/h2&gt;
&lt;p&gt;Business priorities shift - in a healthy market, that's a sign the company is re-aligning itself towards success, responding to changing market conditions, reacting to or getting ahead of a competitor, and so on.&lt;/p&gt;
&lt;p&gt;When business priorities constantly shift, it's a drag. How can I stay motivated on my current project, that's absorbed the attention of management, if I know from experience that in a month we'll have de-prioritized this "critical" project? It says to me that the work is disconnected from business outcomes, and that business outcomes are disconnected from the market.&lt;/p&gt;
&lt;p&gt;Ideally, product work makes tangible impact, and failure to produce has consequences. When work is disconnected from outcomes, when success is disconnected from investment, then lethargy is a natural consequence. This applies not only to actual outcomes but perceived outcomes and company culture: if wins are objectively successful but not celebrated, if failures are not analyzed, then passion is incentivized against.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;Feedback loops permeate my life, from work to home to spirit. Getting a grasp on them, becoming aware and then taking action, has the potential to yield large rewards for small changes.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>The Information</title><link href="https://blog.tmk.name/2024/06/22/the-information/" rel="alternate"></link><published>2024-06-22T00:00:00-04:00</published><updated>2024-06-22T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-06-22:/2024/06/22/the-information/</id><summary type="html">&lt;p&gt;I just finished reading &lt;em&gt;The Information&lt;/em&gt; by James Gleick.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 4 ½ stars.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;This is the third book I've read by Gleick, after &lt;em&gt;Chaos&lt;/em&gt; and &lt;em&gt;Faster&lt;/em&gt;. I found it to be the most ambitious yet, tying together fields of study from history to biology …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I just finished reading &lt;em&gt;The Information&lt;/em&gt; by James Gleick.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 4 ½ stars.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;This is the third book I've read by Gleick, after &lt;em&gt;Chaos&lt;/em&gt; and &lt;em&gt;Faster&lt;/em&gt;. I found it to be the most ambitious yet, tying together fields of study from history to biology to mathematics to communication.&lt;/p&gt;
&lt;p&gt;The central theme is, of course, information. But what is information exactly? That is the question posed and re-posed, answered and answered again by scientists, philosophers and linguists over the centuries.&lt;/p&gt;
&lt;p&gt;I find it a little ironic that trying to summarize the book here is to try to &lt;em&gt;compress its information&lt;/em&gt; - an endeavor attempted broadly by new apps like Blinkist. What does it mean to summarize a book? Can the information be losslessly compressed? I think the repetition, the footnotes, the asides, in a word the &lt;em&gt;redundancy&lt;/em&gt; is part of the learning experience for human beings. If I got anything out of this book, it's to appreciate information density and redundancy.&lt;/p&gt;
&lt;p&gt;That out of the way, the book describes the history, the theory and the flood of information.&lt;/p&gt;
&lt;p&gt;The history is told through the invention and adoption of new information-bearing technologies, namely the telegraph. For the first time in human history, communication could travel faster than a human being. Famously, a criminal jumped onto a train, believing himself to have escaped - only to be met by the police at the next stop, having been informed via telegraph! The telegraph, like written language before it, encoded information, now into dots, dashes and pauses.&lt;/p&gt;
&lt;p&gt;Claude Shannon would go on to develop the laws of information theory in rigorous mathematics. This marked a revolution in the sciences, spreading to biology and physics. In biology, the discovery of DNA marked the transition to understanding life in terms of information transfer, famously espoused by Richard Dawkins in the &lt;em&gt;Selfish Gene&lt;/em&gt;. In physics, information theory led to advancements in the understanding of black holes - eventually resolved by Stephen Hawking - and quantum mechanics.&lt;/p&gt;
&lt;p&gt;Finally, the flood - that is, the modern day human experience of the internet, instantaneous interconnectedness to any person, book, article, movie, song, etc.. It surprises me that that psychologists in the mid century started studying &lt;em&gt;information overload&lt;/em&gt; - without cell phones, email, or push notifications. One key takeaway was a study in which participants kept asking for more information even when it was detrimental to their effectiveness. Perhaps we have a thirst unable to be quenched, because in our history we lived in a dry desert.&lt;/p&gt;
&lt;h2&gt;What I liked&lt;/h2&gt;
&lt;p&gt;Gleick's books make me feel &lt;em&gt;smart&lt;/em&gt; - I attribute that to his skill at science writing, I finish feeling like I learned a great deal. This book was education in breadth as opposed to education in depth with &lt;em&gt;Chaos&lt;/em&gt;. When picking it up, I didn't imagine it'd cover Gödel's theorem, Maxwell's demon, African drum talk, Turing machines, Kolmogorov complexity, quantum mechanics, and the memetics. Gleick managed to weave it all together in a compelling narrative - I had no trouble keeping motivated to finish the book.&lt;/p&gt;
&lt;h2&gt;What I didn't like&lt;/h2&gt;
&lt;p&gt;My only complaint is that the book's ambition may have exceeded its result. In a way, the book itself is flooded with information, and that can drown out the message (how meta!).&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;James Gleick is one of my favorite science authors, and &lt;em&gt;The Information&lt;/em&gt; delivers. Do yourself a favor and check it out.&lt;/p&gt;
&lt;h2&gt;Bonus quote&lt;/h2&gt;
&lt;p&gt;I promised myself I'd record this quote from the book, from Robert Burton in 1621:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I hear new news every day, and those ordinary rumours of war, plagues, fires, inundations, thefts, murders, massacres, meteors, comets, spectrums, prodigies, apparitions, of towns taken, cities besieged in France, Germany, Turkey, Persia, Poland, &amp;amp;c. daily musters and preparations, and such like, which these tempestuous times afford, battles fought, so many men slain, monomachies, shipwrecks, piracies, and sea-fights, peace, leagues, strategems, and fresh alarms. A vast confusion of vows, wishes, actions, edicts, petitions, lawsuits, pleas, laws, proclamations, complaints, grievances are daily brought to our ears. New books every day, pamphlets, currantoes, stories, whole catalogues of volumes of all sorts, new paradoxes, opinions, schisms, heresies, controversies in philosophy, religion, &amp;amp;c. Now come tidings of weddings, maskings, mummeries, entertainments, jubiless, embassies, tilts and tournaments, trophies, triumphs, revels, sports, plays: then again, as in a new shifted scene, treasons, cheating tricks, robberies, enormous villanies in all kinds, funerals, burials, deaths of Princes, new discoveries, expeditions; now comical then tragical matters. To-day we hear of new Lords and officers created, to-morrow of some great men deposed, and then again of fresh honours conferred; one is let loose, another imprisoned; one purchaseth, another breaketh: he thrives, his neighbour turns bankrupt; now plenty, then again dearth and famine; one runs, another rides, wrangles, laughs, weeps &amp;amp;c. Thus I daily hear, and such like.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;All that without Twitter. Nothing new under the sun.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>Code Slop</title><link href="https://blog.tmk.name/2024/06/13/code-slop/" rel="alternate"></link><published>2024-06-13T00:00:00-04:00</published><updated>2024-06-13T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-06-13:/2024/06/13/code-slop/</id><summary type="html">&lt;h2&gt;It all started in code review&lt;/h2&gt;
&lt;p&gt;At work this week I was reviewing code when I came across this piece of code (anonymized):&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;can_be_foobard&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color: #FFD173"&gt;bool&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    Checks if the object is eligible for foobar.&lt;/span&gt;

&lt;span style="color: #7e8aa1"&gt;    :return: True if the object can be foobar&amp;#39;d, False otherwise.&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I immediately suspected …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;It all started in code review&lt;/h2&gt;
&lt;p&gt;At work this week I was reviewing code when I came across this piece of code (anonymized):&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;can_be_foobard&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color: #FFD173"&gt;bool&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    Checks if the object is eligible for foobar.&lt;/span&gt;

&lt;span style="color: #7e8aa1"&gt;    :return: True if the object can be foobar&amp;#39;d, False otherwise.&lt;/span&gt;
&lt;span style="color: #7e8aa1"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I immediately suspected that this was copilot generated: why on Earth would a human repeat themselves 3 times over? This snippet could be reduced to:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;can_be_foobard&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color: #FFD173"&gt;bool&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;:&lt;/span&gt;
    &lt;span style="color: #FFAD66"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The entire docstring is unnecessary! In terms of the information content, the original version is needlessly verbose, providing redundant explanation of the method name. As "slop" is the term &lt;a href="https://simonwillison.net/2024/May/8/slop/"&gt;for unwanted AI-generated content&lt;/a&gt;, this sample falls into the &lt;em&gt;code slop&lt;/em&gt; subcategory.&lt;/p&gt;
&lt;h2&gt;From a previous discussion&lt;/h2&gt;
&lt;p&gt;Last week, my team had a discussion around AI tool best practices. We noted that copilot can be instructed, that is, for comments that describe a task, copilot will suggest an implementation. This is a great experience, as it fits entirely within my editor workflow.&lt;/p&gt;
&lt;p&gt;However, the point was raised that developers should remember to remove those instructions, as they typically don't provide any additional informational content over the code.&lt;/p&gt;
&lt;p&gt;While we're still in the early days of using large language models for software engineering, I foresee some patterns emerging. Engineers are lazy, and will not take time to remove copilot instructions. They may also be prone to mindlessly accept copilot suggestions, as in the docstring example above.&lt;/p&gt;
&lt;p&gt;This suggests to me a trend towards increasing code verbosity via code slop. Combined with existing software tendencies to increase verbosity (not-invented-here syndrome, do-repeat-yourself syndrome, technical rot), software engineers may be in store for misery.&lt;/p&gt;
&lt;h2&gt;And optimism&lt;/h2&gt;
&lt;p&gt;I don't believe it's a guaranteed fate. I find it fascinating that LLMs perform better in discussions and long answers. Perhaps that's due to a low "information density" in the models. If a human can understand a question at face value, without multiple rounds of interaction, then I expect the models will catch up eventually.&lt;/p&gt;
&lt;p&gt;And when they do, model output will be short and sweet, to the point. In the mean time, I'll hold my nose when reviewing code slop.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Three Decades</title><link href="https://blog.tmk.name/2024/06/08/three-decades/" rel="alternate"></link><published>2024-06-08T00:00:00-04:00</published><updated>2024-06-08T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-06-08:/2024/06/08/three-decades/</id><summary type="html">&lt;p&gt;Tomorrow is my thirtieth birthday. I'd like to take the opportunity to reflect.&lt;/p&gt;
&lt;h2&gt;Accomplishments&lt;/h2&gt;
&lt;p&gt;I tried writing this post as a narrative recollection of my twenties, but I don't feel that I'm ready to share that yet. I experienced a lot of pain and conflict that I am still processing …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Tomorrow is my thirtieth birthday. I'd like to take the opportunity to reflect.&lt;/p&gt;
&lt;h2&gt;Accomplishments&lt;/h2&gt;
&lt;p&gt;I tried writing this post as a narrative recollection of my twenties, but I don't feel that I'm ready to share that yet. I experienced a lot of pain and conflict that I am still processing. Instead, I am choosing to share a sample of my accomplishments, highlighting the positives of the past decade.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I returned to and graduated college.&lt;/li&gt;
&lt;li&gt;I have worked steadily for five years, advancing in my career from junior to senior software engineer.&lt;/li&gt;
&lt;li&gt;I have traveled to four new countries: India, Spain, Amsterdam and Belgium.&lt;/li&gt;
&lt;li&gt;I have read about a hundred books.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have more accomplishments, more personal in nature, that I choose to keep to myself and close friends for now. I am still getting comfortable with blogging publicly about my personal life: I find it highly rewarding and liberating, but I am choosing to ease into it.&lt;/p&gt;
&lt;h2&gt;Next decade&lt;/h2&gt;
&lt;p&gt;In the next decade of my life, I have a few high level goals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get married and start a family&lt;/li&gt;
&lt;li&gt;Attempt a leadership role in my career&lt;/li&gt;
&lt;li&gt;Launch my own company/product&lt;/li&gt;
&lt;li&gt;Buy a house&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While each of the above is daunting, I have never felt better equipped. I look forward to blossoming in my thirties.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Travels</title><link href="https://blog.tmk.name/2024/06/01/travels/" rel="alternate"></link><published>2024-06-01T00:00:00-04:00</published><updated>2024-06-01T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-06-01:/2024/06/01/travels/</id><summary type="html">&lt;p&gt;I watched my friend depart on the train this morning, headed back to Spain. It's been a whirlwind two weeks.&lt;/p&gt;
&lt;h2&gt;PyCon&lt;/h2&gt;
&lt;p&gt;I picked up my friend from the airport two weeks ago. It was his first time visiting the United States, and his first chance to meet team mates in …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I watched my friend depart on the train this morning, headed back to Spain. It's been a whirlwind two weeks.&lt;/p&gt;
&lt;h2&gt;PyCon&lt;/h2&gt;
&lt;p&gt;I picked up my friend from the airport two weeks ago. It was his first time visiting the United States, and his first chance to meet team mates in person. We stopped for drinks with a few New York City locals, then headed back to Jersey.&lt;/p&gt;
&lt;p&gt;Early the next day we set out in the car to Pittsburgh, through the beautiful rolling hills of Pennsylvania. Of course, we breakfasted at Cracker Barrel - the most American road trip diner.&lt;/p&gt;
&lt;p&gt;This was my second time in Pittsburgh, but the first in Pittsburgh proper. The city is gorgeous: the meeting of the rivers nestled in the mountains makes for a scenic downtown.&lt;/p&gt;
&lt;p&gt;The conference was excellent, though my highlight was the time spent with coworkers.&lt;/p&gt;
&lt;h2&gt;DC&lt;/h2&gt;
&lt;p&gt;The next stop on the road was Washington, DC. My European friend described it as "European," which is also how a resident of DC described it in comparison to London.&lt;/p&gt;
&lt;p&gt;We toured DC with another friend of mine, also visiting for the first time. I led us on a bike tour of the monuments, along the Mall and the Tidal Basin. As for museums, we wandered the air and space museum (unfortunately partially under renovation), the national gallery of art, the botanical gardens, and the national museum of asian art. My highlight was the Wright brothers exhibit, and the following quote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are looking for perfect safety, you will do well to sit on a fence and watch the birds; but if you really wish to learn, you must mount a machine and become acquainted with its tricks by actual trial.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I also met up with ex-colleagues from my last company. It was nice to reminisce - and also nice to reflect on how much I've grown in a few short years.&lt;/p&gt;
&lt;h2&gt;NYC&lt;/h2&gt;
&lt;p&gt;We drove back to Jersey, with a quick stop in Princeton for lunch. The weather was warm and sunny, the campus lush with ivy.&lt;/p&gt;
&lt;p&gt;The next week was mostly spent in New York. One evening we dined with my siblings. Another we watched Hadestown on broadway - I enjoyed the set design.&lt;/p&gt;
&lt;p&gt;The highlight was Memorial Day, a jam packed day of activities. I tried bouldering for the first time: there's an efficiency of movement that a newbie doesn't grasp. Then, an epic game of chess - me vs 3 friends, from which I came out victorious. Then, my first time at a gun range: good 'murican fun. After, billiards, and finishing with a burger and beer.&lt;/p&gt;
&lt;h2&gt;One takeaway&lt;/h2&gt;
&lt;p&gt;My favorite part of traveling is breaking out of my daily routines. I am susceptible to tunnel vision, to falling into a rut of the well-trodden daily habits.&lt;/p&gt;
&lt;p&gt;I appreciate the broader perspective that comes from new experiences. I am delighted when I discover that I still have more to discover about myself.&lt;/p&gt;
&lt;p&gt;My key takeaway from this trip regards fear. When I was rock climbing, even though I was held securely by the ropes, I could not jump off the wall. I felt the fear viscerally, I willed it but my hands wouldn't budge. Fear is deep rooted, in the "emotional brain" - it cannot be reasoned through. It has to be felt, acknowledged, accepted. It's uncomfortable. But it's not insurmountable.&lt;/p&gt;</content><category term="travel"></category></entry><entry><title>PyCon US '24</title><link href="https://blog.tmk.name/2024/05/25/pycon-us-24/" rel="alternate"></link><published>2024-05-25T00:00:00-04:00</published><updated>2024-05-25T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-05-25:/2024/05/25/pycon-us-24/</id><summary type="html">&lt;p&gt;&lt;img alt="Pycon logo" src="https://blog.tmk.name/images/pycon-us-24.png" /&gt;&lt;/p&gt;
&lt;p&gt;I attended PyCon US '24 with several of my colleagues. This was my second tech conference, following FOSDEM '24 back in February.&lt;/p&gt;
&lt;p&gt;I had a great time at the conference. I was fairly overwhelmed, but thankfully the conference was well organized: the schedule was clear, rooms easy to find, and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="Pycon logo" src="https://blog.tmk.name/images/pycon-us-24.png" /&gt;&lt;/p&gt;
&lt;p&gt;I attended PyCon US '24 with several of my colleagues. This was my second tech conference, following FOSDEM '24 back in February.&lt;/p&gt;
&lt;p&gt;I had a great time at the conference. I was fairly overwhelmed, but thankfully the conference was well organized: the schedule was clear, rooms easy to find, and meals unexpectedly tasty.&lt;/p&gt;
&lt;p&gt;Attending a conference just for the talks misses out on the unique opportunities of meeting in person. After all, the talks will be available online a few weeks later. I tried to put this into action by making the most of the non-talk opportunities.&lt;/p&gt;
&lt;h2&gt;Takeaway: done is better than perfect&lt;/h2&gt;
&lt;p&gt;This was delivered during a lightning talk by a Korean mother who returned to the workforce - an impressive feat in the West, let alone in a culture with stricter gender roles. She described how she started posting videos to youtube to rebuild her tech skills and reach an audience: she would record a video during downtime with her children, and upload without editing.&lt;/p&gt;
&lt;p&gt;This lesson hits me at the core: writing this blog is a challenge, uploading without rounds of revision sounds terrifying! And that's with a readership that I could count on my fingers: me, myself and I 🫠&lt;/p&gt;
&lt;h2&gt;Takeaway: LLM keynote&lt;/h2&gt;
&lt;p&gt;Simon Willison gave an excellent keynote on LLMs. His passion is infectious. He is also a great educator, explaining various topics that I had felt I wouldn't be able to understand.&lt;/p&gt;
&lt;p&gt;For example, he explained Retrieval Augmented Generation (RAG) as just providing extra context for a given query into the LLM input. The extra context may come from a full text search over a given dataset.&lt;/p&gt;
&lt;p&gt;For another example, he explained the security issues with LLMs. Why can't we have autonomous agents that operate on real world input? Because LLMs are susceptible to prompt injection attacks: LLMs can't &lt;em&gt;reliably&lt;/em&gt; distinguish between instructions and content, the data and command channels are combined.&lt;/p&gt;
&lt;h2&gt;Takeaway: open spaces&lt;/h2&gt;
&lt;p&gt;I attended a couple open spaces on security. The first, run by OWASP, was a group card game that helped teach about security. The second, run by PyPI and Github, was a discussion about security in the supply chain.&lt;/p&gt;
&lt;p&gt;I also attended an open space on solo dev work - this took the form of a discussion about managing clients, working through timelines, and general entrepreneurship.&lt;/p&gt;
&lt;p&gt;I found the open spaces to be more engaging than talks, due to the small groups and open discussion.&lt;/p&gt;
&lt;h2&gt;Takeaway: talks&lt;/h2&gt;
&lt;p&gt;I enjoyed the talks on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;implementing a python debugger&lt;/li&gt;
&lt;li&gt;design of everyday APIs&lt;/li&gt;
&lt;li&gt;extensibility in design&lt;/li&gt;
&lt;li&gt;immortal objects&lt;/li&gt;
&lt;li&gt;functional error handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Check them out when the videos are published online.&lt;/p&gt;
&lt;h2&gt;Takeaway: time with colleagues&lt;/h2&gt;
&lt;p&gt;My favorite part of the trip was spending so much time with my colleagues. Meals, talks, and even just hanging out - working on a remote team, the face to face time nurtures relationship development that's just not possible via a computer.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;I look forward to my next conference trip, solo or with team mates. I walk away each time with excitement and energy for my field of work.&lt;/p&gt;</content><category term="career"></category></entry><entry><title>The Saint of Bright Doors</title><link href="https://blog.tmk.name/2024/05/18/the-saint-of-bright-doors/" rel="alternate"></link><published>2024-05-18T00:00:00-04:00</published><updated>2024-05-18T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-05-18:/2024/05/18/the-saint-of-bright-doors/</id><summary type="html">&lt;p&gt;This month's selection for the sci fi reading group is the Nebula award nominee &lt;em&gt;The Saint of Bright Doors&lt;/em&gt; by Vajra Chandrasekera.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 3 stars.&lt;/p&gt;
&lt;h2&gt;Plot&lt;/h2&gt;
&lt;p&gt;This story takes place in a surrealist modern southern India. The protagonist, Fetter, is in group therapy, having been …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This month's selection for the sci fi reading group is the Nebula award nominee &lt;em&gt;The Saint of Bright Doors&lt;/em&gt; by Vajra Chandrasekera.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 3 stars.&lt;/p&gt;
&lt;h2&gt;Plot&lt;/h2&gt;
&lt;p&gt;This story takes place in a surrealist modern southern India. The protagonist, Fetter, is in group therapy, having been groomed to murder his saintly father, the Perfect and Kind.&lt;/p&gt;
&lt;p&gt;The therapy group is for the "almost-chosen" - the world is full of myth, and not all are meant for greatness. It is here that Fetter meets Koel, and joins her revolutionary movement to overthrow the Luriati government.&lt;/p&gt;
&lt;p&gt;Luriat is a fascinating place: it's imbued with magic, whereby closed doors, when unobserved, will transform into doors that can no longer be opened. These doors are traditionally painted bright colors to identify them (hence the name of the book).&lt;/p&gt;
&lt;p&gt;The country is also host to religious, ethnic and caste discrimination. I'm not familiar with the history, but the various occupations suggest possibly British and Mughal rule. Out of these occupations, and mixing with the local traditions, comes religious conflict between the Path Behind and the Path Above (both venerating the Perfect and Kind, but in conflict with one another).&lt;/p&gt;
&lt;p&gt;Fetter's story follows his early adulthood, as he struggles to find his way in the world. He moved from his home town in Acusbad to Luriat. He dates, but is unable to lower his guard, keeping his partners at a distance. Fetter keeps &lt;em&gt;everyone&lt;/em&gt; at a distance, including his mother (named Mother of Glory) and the therapy group: Fetter tells no one that he sees demons, antigods - the invisible laws and powers of the world. These creatures stalk the earth, bringing plague and ill portents.&lt;/p&gt;
&lt;p&gt;Fast forwarding through the story, Fetter goes undercover to study the bright doors for the revolutionary movement, all the while making connections to the powerful of Luriat. The Perfect and Kind is coming to Luariat to reconcile the warrning Paths. An artifact is discovered which has the power to kill the Perfect and Kind. Fetter steals the artifact, and sets out on a mad dash to kill his father. He makes a final visit to Mother of Glory on her deathbed.&lt;/p&gt;
&lt;p&gt;The assassination attempt fails - the Perfect and Kind rewrote history to avoid it. Mother of Glory explains the backstory to Fetter: originally, the Perfect and Kind was just a prince from another land. He learned the powers of magic from her people. In order to escape her curse, he rewrote history to bridge the island to the mainland, such that the island had &lt;em&gt;always&lt;/em&gt; been connected to the mainland.&lt;/p&gt;
&lt;p&gt;Fetter attempts to return to Luriat, but is arrested at the border. He is sent to a prison camp, which he wanders for possibly months before finally being freed by the Perfect and Kind.&lt;/p&gt;
&lt;p&gt;Back in Luriat, Fetter seemingly submits to his father's wishes to follow in his footsteps. All the while, we discover there's a surprise first person narrator - Fetter's shadow, which had been taken from him as a child. The shadow kills the Perfect and Kind, and Fetter rejoins the revolutionary movement.&lt;/p&gt;
&lt;h3&gt;Parallels&lt;/h3&gt;
&lt;p&gt;Thanks to some reviews I learned that this story closely parallels a story about the Buddha, in which he leaves his wife and child to start the ascetic life. Thus the name "Fetter", or (worldly) "shackles." I didn't pick up on this myself, being unfamiliar with that story.&lt;/p&gt;
&lt;h2&gt;What I liked&lt;/h2&gt;
&lt;p&gt;While the first part dragged on, eventually I couldn't put this book down. I am reminded of Roger Zelazny: a world of demi-gods and magic, yet thoroughly modern. I enjoyed the reality of the mythology: the different "almost-chosen" characters living in the shadow of their individual destinies.&lt;/p&gt;
&lt;p&gt;I liked the limited use of magic, especially world-shattering magic: the Perfect and Kind changes history just twice, each time to escape his demise. Afterwards, he acknowledges that such a powerful magic has unintended consequences: changing a river upstream may cause radical changes downstream (hello, chaos theory!). Thus his Path splits and fractures.&lt;/p&gt;
&lt;p&gt;The prison sequence was well written. The description of the camp was evocative: the misery and the lack of coordination; Fetter's inability to find his particular zone, until he stumbles upon it; the mini worlds within the camp walls. It was thoroughly Kafkaesque, and a great representation of the horrors experienced by real people in real camps.&lt;/p&gt;
&lt;p&gt;Theme-wise, I appreciated the author's attention on identity, childhood trauma, and destiny. Fetter is clearly traumatized: being raised to kill his father by his vindictive mother may be a simple allegory for a child raised by a single mother who hates the deadbeat father. Fetter struggles to define his own identity: he finds himself pulled into identities prepared &lt;em&gt;for him by others&lt;/em&gt;, including the neighborhood "helper", the undercover role, the revolutionary, the boyfriend, assassin, and "chosen."&lt;/p&gt;
&lt;h2&gt;What I didn't like&lt;/h2&gt;
&lt;p&gt;While I have many positive words for the book, the ending ruined my enjoyment. Fetter's shadow played a grossly ridiculous deus ex machina, denying Fetter the opportunity to make the decision to kill his father or not. Fetter falling back into the revolutionary movement was disappointing: he remained living in the interests of grand ideals without reconciling his own inner emptiness.&lt;/p&gt;
&lt;p&gt;That's to say that the themes, while explored, failed to be developed to a pleasing (whether positive or negative) conclusion - the catharsis was missing. Fetter's childhood trauma isn't eliminated by the death of his parents, nor does he construct a new identity through it. It's as if the author was unable to weave a worthy ending from the various threads employed.&lt;/p&gt;
&lt;p&gt;Seriously. The last twenty pages let me down.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;This book had potential, which makes its ending so frustrating. I would still recommend the book to those interested in blending modernity with fantasy, but with the caveat to keep expectations low with regards to the ending.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>Good Software Tools</title><link href="https://blog.tmk.name/2024/05/11/good-software-tools/" rel="alternate"></link><published>2024-05-11T00:00:00-04:00</published><updated>2024-05-11T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-05-11:/2024/05/11/good-software-tools/</id><summary type="html">&lt;p&gt;As a programmer, I spend a significant amount of time writing and using software. I'd like to list a few of my favorite tools.&lt;/p&gt;
&lt;h2&gt;Philosophy&lt;/h2&gt;
&lt;p&gt;I prefer to use open source, cross platform, local software. Old and boring is a good signal. Community driven over VC funded projects.&lt;/p&gt;
&lt;p&gt;I don't …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As a programmer, I spend a significant amount of time writing and using software. I'd like to list a few of my favorite tools.&lt;/p&gt;
&lt;h2&gt;Philosophy&lt;/h2&gt;
&lt;p&gt;I prefer to use open source, cross platform, local software. Old and boring is a good signal. Community driven over VC funded projects.&lt;/p&gt;
&lt;p&gt;I don't hop from fad editor to fad editor as popularity rises and falls. I don't worry about vendor lock-in. I don't worry about Embrace-Extend-Extinguish.&lt;/p&gt;
&lt;p&gt;I aim to keep my tooling simple, to choose software that gets out of my way, so that I can focus on getting my work done.&lt;/p&gt;
&lt;h2&gt;Emacs&lt;/h2&gt;
&lt;p&gt;I've been using Emacs for more than a decade. That said, I'm not an elisp hacker - I use Doom to provide an excellent out-of-the-box experience, without requiring &lt;em&gt;too much&lt;/em&gt; manual setup.&lt;/p&gt;
&lt;p&gt;My favorite aspect of Emacs is the keyboard-focus: I rarely have to use the mouse. It's an editor that gets out of my way.&lt;/p&gt;
&lt;p&gt;Within Emacs, a few of my favorite tools are magit, projectile, LSP mode and avy.&lt;/p&gt;
&lt;p&gt;I love &lt;a href="https://magit.vc/"&gt;magit&lt;/a&gt; - for my day to day git operations, committing, branching, pushing and pulling - it's an absolute breeze.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/bbatsov/projectile"&gt;Projectile&lt;/a&gt; makes working within projects, typically git repositories, so easy. My frequent project related commands are memorized - find file in project, search in project.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/emacs-lsp/lsp-mode"&gt;LSP mode&lt;/a&gt; - support for IDE-agnostic language servers - has been a godsend. No longer do I have to learn the specific nuances for each programming language mode, it's all standardized under LSP. Since language servers are developed independently, they also tend to be more feature complete than standalone programming modes.&lt;/p&gt;
&lt;p&gt;Lastly, special shoutout to &lt;a href="https://github.com/abo-abo/avy"&gt;Avy&lt;/a&gt; and &lt;code&gt;avy-goto-word-1&lt;/code&gt;. At first I didn't understand it, but when I understood the &lt;em&gt;zen&lt;/em&gt;, that is, "look at where you want to go when you invoke the command," it all fell into place. I use this frequently (bound to &lt;code&gt;M-s&lt;/code&gt;) to navigate.&lt;/p&gt;
&lt;h3&gt;AI Tools&lt;/h3&gt;
&lt;p&gt;Special shoutout to &lt;a href="https://github.com/copilot-emacs/copilot.el"&gt;copilot.el&lt;/a&gt; for providing seamless integration with Github copilot. I am also starting to use &lt;a href="https://github.com/karthink/gptel"&gt;gptel&lt;/a&gt;, early results are positive but I've yet to really get in the groove.&lt;/p&gt;
&lt;h2&gt;Tmux + Tmuxinator&lt;/h2&gt;
&lt;p&gt;This may be controversial, but I don't put &lt;em&gt;everything&lt;/em&gt; inside Emacs. I know it's possible to turn Emacs into a &lt;a href="https://en.wikipedia.org/wiki/Trapper_Keeper_(South_Park)"&gt;Trapper Keeper&lt;/a&gt;, absorbing all use cases, but I don't think it's always the right move.&lt;/p&gt;
&lt;p&gt;As a primarily desktop user, I turn off/on my computer every day. I quickly felt the pain of having to set up my terminal windows every morning, and found the solution in &lt;a href="https://github.com/tmuxinator/tmuxinator"&gt;tmuxinator&lt;/a&gt;. With tmuxinator, I create templates for my projects, which stand up to prepare my environment. For example, at work, my tmuxinator config includes: jupyter lab server, django runserver, celery worker, webpack, and docker compose up.&lt;/p&gt;
&lt;p&gt;I also think these templates are great for sharing with new team members: even if they don't want to use tmux, it's easily translatable to their preferred environment.&lt;/p&gt;
&lt;h2&gt;Docker Compose&lt;/h2&gt;
&lt;p&gt;Just like with tmuxinator, docker compose is a templated tool for running project dependencies. I use it for local project dependencies on linux, and my personal project deployments on my Raspberry Pi.&lt;/p&gt;
&lt;p&gt;It's a fantastic tool that solves a tough problem: how do I maintain different versions of dependencies (postgres, redis, elastic search, etc.) when working on different projects? With docker compose, I simply don't worry about spending time installing and configuring stack dependencies. Just run &lt;code&gt;docker compose up&lt;/code&gt; and start working.&lt;/p&gt;
&lt;h2&gt;Pelican&lt;/h2&gt;
&lt;p&gt;I can't end this post without mentioning the software that powers this blog - &lt;a href="https://getpelican.com/"&gt;Pelican&lt;/a&gt;. It's another example of software that gets out of my way. I write my blog posts in Markdown, keep the blog backed up on Github, and upload via rsync to my Raspberry Pi. It meets all of my requirements for good software I can trust.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is just a taste of the software I use on a daily basis, for work and personal projects. I am documenting it primarily for future retrospective: in a decade, I wonder what software I'll be using - perhaps I'll just be providing quality assurance for AI generated code on some locked down corporate system. Hopefully not!&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Mythology</title><link href="https://blog.tmk.name/2024/05/04/mythology/" rel="alternate"></link><published>2024-05-04T00:00:00-04:00</published><updated>2024-05-04T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-05-04:/2024/05/04/mythology/</id><summary type="html">&lt;p&gt;&lt;img alt="Jason and the Argonauts" src="https://blog.tmk.name/images/jason-and-the-argonauts.webp" /&gt;&lt;/p&gt;
&lt;p&gt;I just wrapped up Edith Hamilton's &lt;em&gt;Mythology&lt;/em&gt;, a staple of high school English classrooms (well, not &lt;em&gt;my&lt;/em&gt; English classrooms, from what I remember).&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 5 stars.&lt;/p&gt;
&lt;h2&gt;Outline and perspectives&lt;/h2&gt;
&lt;p&gt;This book details the Greek pantheon, ending with a short chapter on the Norse religion. I …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="Jason and the Argonauts" src="https://blog.tmk.name/images/jason-and-the-argonauts.webp" /&gt;&lt;/p&gt;
&lt;p&gt;I just wrapped up Edith Hamilton's &lt;em&gt;Mythology&lt;/em&gt;, a staple of high school English classrooms (well, not &lt;em&gt;my&lt;/em&gt; English classrooms, from what I remember).&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 5 stars.&lt;/p&gt;
&lt;h2&gt;Outline and perspectives&lt;/h2&gt;
&lt;p&gt;This book details the Greek pantheon, ending with a short chapter on the Norse religion. I had previously read Hamilton's &lt;em&gt;The Greek Way&lt;/em&gt;, which I enjoyed, and which made for a good complement to this book. Clearly, Hamilton is passionate about the ancient Greeks and their culture - and that passion is infectious. While I've been attending a Buddhist monastery for about a year now, learning and practicing, I still find myself conflicted between the two seemingly contradictory philosphies. The heroism of the ancient Greeks - the striving, the proof of excellence in deeds, is contrasted by the heroism of the Buddhists - the self mastery in renunciation. I do find overlap, there is striving for excellence and mastery in each, but it's in the way that each affirms life. More to come I am sure!&lt;/p&gt;
&lt;p&gt;Across the various myths and stories a few stood out to me, that I'll record for posterity.&lt;/p&gt;
&lt;h3&gt;Prometheus&lt;/h3&gt;
&lt;p&gt;So many of the stories rounded out my knowledge of common myths. Prometheus, I was aware, stole fire from the gods and gave it to mankind. I was not aware that he was considered in general a benefactor of mankind, that his name meant "forethought" and he was celebrated for his wisdom. I was also surprised to hear of his determination despite his suffering. When offered the opportunity to escape his torment by revealing the one who would dethrone Zeus, Prometheus said to go and convince the sea not to break, it would be easier than convincing him. That unbreakable will in adversity is powerful to consider in my own life of comfort and ease.&lt;/p&gt;
&lt;h3&gt;Heracles&lt;/h3&gt;
&lt;p&gt;I vaguely remember watching the Hercules movie as a child. Yes indeed, Heracles strangled two snakes as a babe. But he also had a darker, human side. He was the strongest man in the world, as strong as even the gods. But he was prone to mishap: whether through divine manipulation or carelessness, he caused harm to a great many people. Perhaps the greatest strength of Heracles was his conscience: wracked with guilt, he would do anything to make amends - most famously his 12 labors, after murdering his family.&lt;/p&gt;
&lt;h3&gt;Jason and the Argonauts&lt;/h3&gt;
&lt;p&gt;Per the post image, I just had to watch the classic movie. I greatly enjoyed it - the animation was honestly more impressive than most CGI. I thought the scale of the monsters was well executed: it reminded me of Dark Souls!&lt;/p&gt;
&lt;p&gt;Beyond the movie, I liked the myth, too. How inspiring that, given an impossible quest, Jason charges out into the world, trusting in himself that he will figure it out. That upon hearing of the quest, all the heroes of Greece join. I greatly enjoy such tales of unbridled courage and optimism.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This book has been sitting on my shelf for a few years. I am so happy to have picked it up. I read it at just the right time I am sure, when I was prepared to take away as much as I did.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>Babel</title><link href="https://blog.tmk.name/2024/04/27/babel/" rel="alternate"></link><published>2024-04-27T00:00:00-04:00</published><updated>2024-04-27T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-04-27:/2024/04/27/babel/</id><summary type="html">&lt;p&gt;&lt;img alt="Lost in translation" src="https://blog.tmk.name/images/lost-in-translation.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;For this month's science fiction and fantasy book club, I read Babel by R. F. Kuang.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 2 stars.&lt;/p&gt;
&lt;h2&gt;Plot&lt;/h2&gt;
&lt;p&gt;This story follows the life of Robin, an immigrant to England in the early 1800s. Okay, immigrant hides the details: his father, Richard Lovell, a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="Lost in translation" src="https://blog.tmk.name/images/lost-in-translation.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;For this month's science fiction and fantasy book club, I read Babel by R. F. Kuang.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 2 stars.&lt;/p&gt;
&lt;h2&gt;Plot&lt;/h2&gt;
&lt;p&gt;This story follows the life of Robin, an immigrant to England in the early 1800s. Okay, immigrant hides the details: his father, Richard Lovell, a white British man, took Robin from China, at a young age, after the death of Robin's Chinese family. Lovell never acknowledges Robin as his own, raising Robin to become a translator in service of the British empire.&lt;/p&gt;
&lt;p&gt;Which is a good segue to the core of the book: translation is the key to magic, in this alternate history. Translation from one language to another, some word or phrase, is never perfect, never one-to-one.&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; Something is lost in the process. That &lt;em&gt;something&lt;/em&gt; becomes &lt;em&gt;magic&lt;/em&gt;. The practical requirements of the magic is to inscribe the words into silver bars, and then speak the words (requiring fluency in each language for it to work). For the British, translating between diverse languages such as English, Mandarin and Urdu provides enormous opportunity for new magical applications.&lt;/p&gt;
&lt;p&gt;Robin spends his formative years studying, isolated away from the world by Lovell. Then, he goes to Oxford, to study at the prestigious Institute of Translation, whose building is named Babel. At Oxford, Robin befriends his cohort, fellow translation students: Ramy, Letty, and Victoire. Ramy and Victoire are also transplants from their native countries, and all four bond over their experiences at Babel.&lt;/p&gt;
&lt;p&gt;Almost immediately upon arriving at Oxford, Robin meets his doppelganger in the midst of a crime: Griffin is Robin's half brother, Lovell's previous attempt to bring Chinese students to Babel. Griffin has however left academia to fight against the tyranny of Babel and all it stands for and supports, namely the British empire. Griffin belongs to a group named the Hermes society, an underground movement fighting against the British empire.&lt;/p&gt;
&lt;p&gt;Fast forward 3 years and several hundred pages.&lt;/p&gt;
&lt;p&gt;Robin kills Lovell in the heat of an argument. This forces the four students to go into hiding, linking up with the Hermes society. Hermes gears up its fight to prevent war between Britain and China. Letty betrays Hermes, leading to the death of Ramy. Griffin breaks Robin and Victoire out of jail, and dies in the process.&lt;/p&gt;
&lt;p&gt;Robin and Victoire occupy Babel, joined by sympathetic students and teachers. Without Babel, silver-magic-powered carts, roads, mills, bridges and so on start to malfunction or collapse. British troops siege the tower. Victoire leaves, and Robin destroys the tower by translating the untranslatable - &lt;em&gt;translation&lt;/em&gt; itself - leading to a chain reaction as all the silver in the tower explodes.&lt;/p&gt;
&lt;h2&gt;What I liked&lt;/h2&gt;
&lt;p&gt;The core of the plot has potential. A group of students, overcome with sympathy for an oppressed people, hatch a scheme to overthrow the seat of magical power in an empire. As the crisis of war looms, and peaceful efforts fail, facing internal betrayal, the remaining few sacrifice themselves to blow up the tower of Babel - representing both a physical and metaphysical attack on the empire.&lt;/p&gt;
&lt;p&gt;Unfortunately, what I liked ends there - the potential was there, but the execution faltered.&lt;/p&gt;
&lt;h2&gt;What I didn't like&lt;/h2&gt;
&lt;p&gt;The pacing of the story ruined any possibility of enjoyment. As even a positive reviewer described it, the book is &lt;em&gt;dry&lt;/em&gt;. Several hundred pages could have been summarized and shortened to produce a better rhythm. The comparison with the most recent book I read, Embassytown, is stark: whereas Embassytown was too dense for its own good, Babel is too long.&lt;/p&gt;
&lt;p&gt;Another annoyance is the footnotes. Some footnotes explain a particular translation - fair enough. But others provide character motivations, background, history, etc. - content that should be woven into the main text! I see it as a failure on the author's part if they rely on footnotes. For perspective, I have read some works of Richard Francis Burton, so I am &lt;em&gt;well versed&lt;/em&gt; in footnotes filling more than half of a page. His footnotes are decorative, optional, complementary - not providing core context to the plot.&lt;/p&gt;
&lt;p&gt;I also felt the characters were one dimensional. Either they were on the side of the good, or on the side of evil. And the author left no room for ambiguity. I had no doubt which way Robin's sympathies would turn as the novel grinded on. The betrayal by Letty felt awkward and forced, shoe-horned in to make a point. I would have appreciated more depth to Lovell, who plays such a crucial role in the story: how did he come to be so evil?&lt;/p&gt;
&lt;p&gt;Finally, I was frustrated by the loose ends of the plot. Robin's family dies in a disease outbreak - but Lovell is conveniently nearby? It's implied the realization is that Lovell could have &lt;em&gt;saved&lt;/em&gt; Robin's family - but I am left wondering, did Lovell intentionally kill Robin's family? The other loose end was the romance between Robin and Ramy - it's only hinted upon a few times, but it's just begging for exploration. Why not develop the romance? At least to satisfy the readers' curiosity.&lt;/p&gt;
&lt;h2&gt;The in-between&lt;/h2&gt;
&lt;p&gt;One aspect I struggled with was how to interpret the story. Robin is traumatized an early age with the loss of his family, and then continually abused and neglected by Lovell. Robin murders his own father, falls in with a band of violent revolutionaries, and sacrifices himself for the movement. This is a terribly tragic story, and arguably motivates the question of the subtitle: the necessity of violence. When I think of violence in history, of strikes, revolts and revolutions, I don't take into account the tragedies that put the actors on that specific stage. I focus on the poetry of high ideals, of justice and courage and brotherhood.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;All in all, I don't recommend this book - it's not worth the time investment to read. &lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;As my friend pointed out, this is true within a language, too - all communication is imperfect. The only way for it to be perfect would be direct access to each other's brains... which clarifies for me the Ariekei language in Embassytown.&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="books"></category></entry><entry><title>Lost Language</title><link href="https://blog.tmk.name/2024/04/20/lost-language/" rel="alternate"></link><published>2024-04-20T00:00:00-04:00</published><updated>2024-04-20T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-04-20:/2024/04/20/lost-language/</id><summary type="html">&lt;p&gt;Some weeks ago I read an excellent article by Alastair Humphreys, entitled &lt;a href="https://www.noemamag.com/a-single-small-map-is-enough-for-a-lifetime/"&gt;A Single Small Map Is Enough For A Lifetime&lt;/a&gt;. The core of the article details how much excitement and wonder exists right outside one's own door - the thirst for adventure can be quenched without boarding a CO2 emitting …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Some weeks ago I read an excellent article by Alastair Humphreys, entitled &lt;a href="https://www.noemamag.com/a-single-small-map-is-enough-for-a-lifetime/"&gt;A Single Small Map Is Enough For A Lifetime&lt;/a&gt;. The core of the article details how much excitement and wonder exists right outside one's own door - the thirst for adventure can be quenched without boarding a CO2 emitting plane to a far off destination.&lt;/p&gt;
&lt;p&gt;I have heard the carbon argument against travel before, but it doesn't move me. Global warming and climate change will not be addressed through changes in individual consumer behavior. That said, this disagreement in motivation doesn't detract from my agreement with the subsequent points.&lt;/p&gt;
&lt;p&gt;I have moved almost yearly since starting college. I live away from my hometown. I don't feel strongly connected to my locality. After so much movement, I struggle to connect to an area, as I have internalized a semi-nomadic existence. I dream of moving again at the end of my lease, somewhere distant and new.&lt;/p&gt;
&lt;p&gt;This paragraph from the article struck me, and motivated this post:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Once I can put a name to something, like a bird or tree, I seem to come across it more often, and I also appreciate it more for knowing the word. As Robert Macfarlane wrote in “Landmarks,” “Language deficit leads to attention deficit. As we further deplete our ability to name, describe and figure particular aspects of our places, our competence for understanding and imagining possible relationships with non-human nature is correspondingly depleted.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I can share two anecdotes which validated this perspective for me.&lt;/p&gt;
&lt;p&gt;I recently bought a snake plant, my first house plant. Suddenly, I see snake plants everywhere, including in places I've walked by dozens of times. It's as if my world has become more real, more alive, &lt;em&gt;inhabited&lt;/em&gt; as opposed to desolate - and just from a single houseplant.&lt;/p&gt;
&lt;p&gt;Secondly, in reading Richard Francis Burton's  &lt;em&gt;Personal narrative of a pilgrimage to el Medinah and Meccah&lt;/em&gt;, I took away the incredible breadth of the author's vocabulary. Perhaps not surprising for a master of languages, but it extended beyond casual conversation to poetry to literature to religion to &lt;em&gt;geography and geology&lt;/em&gt; - describing each leg of the trip in exquisite detail.&lt;/p&gt;
&lt;p&gt;I feel this language deficit deeply. I can see how it limits my perception, and how that limited perception affects my psychology.&lt;/p&gt;
&lt;p&gt;I don't believe there's an easy cure. The fast-paced, instant-cure lifestyle led me to this situation, so I don't think it will help me escape. Instead, I want to slow down. Read more slowly, consulting a dictionary for unfamiliar words. Write and reflect more. Engage in conversation with others. Break out of my comfort zone, before it slowly strangles itself in language deficit.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Timezones</title><link href="https://blog.tmk.name/2024/04/13/timezones/" rel="alternate"></link><published>2024-04-13T00:00:00-04:00</published><updated>2024-04-13T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-04-13:/2024/04/13/timezones/</id><summary type="html">&lt;p&gt;My company services several cities in the continental United States. For those major metropolitan cities, we only have to concern ourselves with three US timezones: US/Pacific, US/Central, and US/Eastern. Even without non-hour-offset timezones I still experience headaches on a weekly basis. Many stem from original sins within …&lt;/p&gt;</summary><content type="html">&lt;p&gt;My company services several cities in the continental United States. For those major metropolitan cities, we only have to concern ourselves with three US timezones: US/Pacific, US/Central, and US/Eastern. Even without non-hour-offset timezones I still experience headaches on a weekly basis. Many stem from original sins within the code, the legacy of unfulfilled future-proofing, failed prophecies.&lt;/p&gt;
&lt;p&gt;In this article I'll describe a couple experiences and lessons learned. Note that my experiences are primarily within the context of Python and Django.&lt;/p&gt;
&lt;h2&gt;datetime(..., tzinfo=mytz)&lt;/h2&gt;
&lt;p&gt;First of all, shoutout to Paul Ganssle's excellent article &lt;a href="https://blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.html"&gt;pytz: The Fastest Footgun in the West&lt;/a&gt;. It came as quite the surprise that passing a pytz timezone to the &lt;code&gt;datetime&lt;/code&gt; constructor is incorrect - violating the principle of least surprise. It's easy enough to remedy, but it's a terrible pitfall.&lt;/p&gt;
&lt;p&gt;I had premonitions that something was off, when I saw the strange timezone offsets, with &amp;plusmn; a few minutes tacked onto the expected hours. I'm not completely caught up with the transition to ZoneInfo, but I believe that the situation moving forward is vastly improved, and a ZoneInfo timezone can safely be passed to the &lt;code&gt;datetime&lt;/code&gt; constructor.&lt;/p&gt;
&lt;h2&gt;Datetimes and dates&lt;/h2&gt;
&lt;p&gt;When first starting out working with timezones, it's easy to naively try &lt;code&gt;timestamp.date()&lt;/code&gt;. The issue of course is that timestamps are universal "points in time", while dates are not: today, April 13, 2024, is only 24 hours long from the &lt;em&gt;perspective of a given timezone&lt;/em&gt;. Without the timezone, a date is ambiguous.&lt;/p&gt;
&lt;p&gt;I cut my teeth on this at my internship, where I created a metrics tracking dashboard. The dashboard was designed to report the number of events in a given day. But there was feedback that the metrics were different between East and West coast users. Which makes sense, if the report is using the user's timezone for aggregating timestamps &lt;em&gt;by date&lt;/em&gt;. An event at 1am in New York will fall on different dates for West and East coast users.&lt;/p&gt;
&lt;p&gt;The solution, therefore, depends on the situation and the requirements. Perhaps timestamps should be converted to the originating timezone's date. Or perhaps the user's timezone. Rarely, though, should a date be extracted from a timestamp without timezone conversion.&lt;/p&gt;
&lt;h2&gt;Naive datetimes&lt;/h2&gt;
&lt;p&gt;This continues to plague me at work, original sins from developers long since moved on.&lt;/p&gt;
&lt;p&gt;At the Django docs' &lt;a href="https://docs.djangoproject.com/en/5.0/topics/i18n/timezones/#code"&gt;suggestion&lt;/a&gt;, I added the following to the codebase, in order to transform naive datetime warnings into errors:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;warnings&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;filterwarnings(&lt;/span&gt;
    &lt;span style="color: #D5FF80"&gt;&amp;quot;error&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
    &lt;span style="color: #F29E74"&gt;r&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;DateTimeField .* received a naive datetime&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
    &lt;span style="color: #73D0FF"&gt;RuntimeWarning&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
    &lt;span style="color: #F29E74"&gt;r&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;django\.db\.models\.fields&amp;quot;&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This exposed so many subtle timezone issues, that the team quickly gave up and commented out the code within our local repos.&lt;/p&gt;
&lt;p&gt;One of the most nefarious issues is setting a DateTimeField to a &lt;em&gt;date&lt;/em&gt;. When persisted, this converts to midnight on the date &lt;em&gt;in the server default timezone&lt;/em&gt;. Note that in Django, the &lt;em&gt;default&lt;/em&gt; timezone is separate from the &lt;em&gt;active&lt;/em&gt; timezone. At work, the &lt;em&gt;default&lt;/em&gt; timezone is US/Pacific. I expect it would be a Herculean effort to clean this up. In the meantime, there's a long paragraph of text warning developers "here be dragons."&lt;/p&gt;
&lt;h2&gt;self.resolved_at = timezone.localtime()&lt;/h2&gt;
&lt;p&gt;This is harmless, but a personal pet peeve. Per the Django docs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When support for time zones is enabled, Django stores datetime information in UTC in the database, uses time-zone-aware datetime objects internally, and translates them to the end user’s time zone in templates and forms.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Django stores datetime information in UTC in the database. Consider this method on a Django model:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;resolve&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;):&lt;/span&gt;
  &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;resolved_at&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;timezone&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;localtime(timezone&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;timezone)&lt;/span&gt;
  &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;save()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;First, it looks up the &lt;code&gt;timezone&lt;/code&gt; property on the object. Then, &lt;code&gt;timezone.localtime()&lt;/code&gt; gets the current time, converting it to the specified timezone. When calling &lt;code&gt;save()&lt;/code&gt;, the datetime is converted &lt;em&gt;back to UTC&lt;/em&gt; when it is persisted &lt;em&gt;as UTC&lt;/em&gt; in the database. So many unnecessary steps and translations!&lt;/p&gt;
&lt;p&gt;Timestamps should be in UTC, and converted to a desired timezone at usage, just as Django does internally.&lt;/p&gt;
&lt;h2&gt;Recommended reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.djangoproject.com/en/5.0/topics/i18n/timezones/"&gt;Django timezone support documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.html"&gt;pytz: The Fastest Footgun in the West&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="programming"></category></entry><entry><title>Embassytown</title><link href="https://blog.tmk.name/2024/04/06/embassytown/" rel="alternate"></link><published>2024-04-06T00:00:00-04:00</published><updated>2024-04-06T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-04-06:/2024/04/06/embassytown/</id><summary type="html">&lt;p&gt;For this month's science fiction and fantasy book club, I read Embassytown by China Miéville.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 3 ½ stars.&lt;/p&gt;
&lt;h2&gt;Plot&lt;/h2&gt;
&lt;p&gt;Avice, a human, grows up in the human neighborhood of a city on Arieke, an alien planet at the edge of known space. She is …&lt;/p&gt;</summary><content type="html">&lt;p&gt;For this month's science fiction and fantasy book club, I read Embassytown by China Miéville.&lt;/p&gt;
&lt;p&gt;tl;dr I rate the book 3 ½ stars.&lt;/p&gt;
&lt;h2&gt;Plot&lt;/h2&gt;
&lt;p&gt;Avice, a human, grows up in the human neighborhood of a city on Arieke, an alien planet at the edge of known space. She is the protagonist from whose perspective we witness the events that unfold; she also plays a critical role in the denouement.&lt;/p&gt;
&lt;p&gt;The natives of Arieke are called Hosts or Ariekes. They are totally alien, in biology, culture, technology, and most importantly, Language.&lt;/p&gt;
&lt;p&gt;Language is unique in a few ways. One, the Ariekes only understand Language when spoken by a creature with a mind: reproductions via technology may make the correct sound-waves but are gibberish to Hosts. This suggests that language has some metaphysical quality uniquely created by a biological creature. Two, the Ariekes have two mouths, so the language is spoken as two streams of phonemes in parallel. This means that a single human cannot speak Language, so in order to communicate, twins are genetically engineered and then linked via technology to be able to speak Language.&lt;/p&gt;
&lt;p&gt;There's a lot to Language; it's the core plot device of the novel.&lt;/p&gt;
&lt;p&gt;The genetically engineered twins are called Ambassadors. A new Ambassador arrives on Arieke, a unique Ambassador: EzRa are not twins, but in fact despise each other. This contradiction between the minds speaking Language creates a sensation so strong in the Ariekes that they quickly become addicts. Bear with me, this is only a brief summary, so if it sounds ridiculous..&lt;/p&gt;
&lt;p&gt;Arieken society collapses practically overnight as the addiction spreads. A movement within the Ariekes begins which cures the addiction by physically removing their own ears: this movement spreads by violence as the cured, named the Absurd, seek to inoculate their brethren.&lt;/p&gt;
&lt;p&gt;Finally, Avice manages to teach the Ariekes to lie - I forgot to mention that Language is spoken Truth, the signifier and the signified are not separate - which cures the Ariekes of their addiction. The Absurd and the Liars learn to communicate in writing overnight and it's a happy ending.&lt;/p&gt;
&lt;h2&gt;What I liked&lt;/h2&gt;
&lt;p&gt;The world-building and universe-building are mostly excellent. I liked that Ariekes is an outpost colony at the edge of the immer. Instantaneous communication is impossible: "messages in a bottle" (or &lt;em&gt;miabs&lt;/em&gt;) are sent across space physically. I liked the lighthouse in the immer, and the Wreck of a previous ship orbiting Arieke.&lt;/p&gt;
&lt;p&gt;I really enjoyed the biorigging - the planet, the city, the buildings are &lt;em&gt;alive&lt;/em&gt;, combinations of technology and biology, carefully crafted by the Ariekes. This alone made for a rich setting.&lt;/p&gt;
&lt;h2&gt;What I didn't like&lt;/h2&gt;
&lt;p&gt;This novel is too ambitious, it should have been split up across a series. There are multiple stories at multiple levels advancing in parallel, and it's simply too much for such a short book. For example, the empire-colony plotline, Avice's personal story, Language and the Ariekes, the spread of the addiction and the collapse of society. I'd have preferred the book ended with the start of the addiction, and then the next worked through its development and resolution.&lt;/p&gt;
&lt;p&gt;That brings me to the ending.. I can't buy it. Overnight the Ariekes are transformed as they are taught how to lie, at just the last second.. deus ex machina as someone in the book club called it. This allowed for the happy ending, but given the plot up to that point, I think the ending should have been collapse and rebirth. The addiction spreads and consumes the Ariekes. The next human ship arrives and finds the colony destroyed: Ehrsul, a robot, the only survivors, tells the story of the collapse.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you're interested in an intellectually challenging science fiction novel that focuses on themes of language, then I recommend the book to you. &lt;/p&gt;</content><category term="books"></category></entry><entry><title>Interviewing</title><link href="https://blog.tmk.name/2024/03/30/interviewing/" rel="alternate"></link><published>2024-03-30T00:00:00-04:00</published><updated>2024-03-30T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-03-30:/2024/03/30/interviewing/</id><summary type="html">&lt;p&gt;I have been helping my company interview, which makes for a great opportunity to elaborate on my experiences and perspectives: what do I look for when I interview a candidate?&lt;/p&gt;
&lt;h2&gt;Green flags&lt;/h2&gt;
&lt;p&gt;These are a collection of positive qualities and behaviors that I look for in a candidate.&lt;/p&gt;
&lt;h3&gt;Online presence …&lt;/h3&gt;</summary><content type="html">&lt;p&gt;I have been helping my company interview, which makes for a great opportunity to elaborate on my experiences and perspectives: what do I look for when I interview a candidate?&lt;/p&gt;
&lt;h2&gt;Green flags&lt;/h2&gt;
&lt;p&gt;These are a collection of positive qualities and behaviors that I look for in a candidate.&lt;/p&gt;
&lt;h3&gt;Online presence&lt;/h3&gt;
&lt;p&gt;Typically the first impression for a candidate is a resume. A clean and straightforward resume is ideal: don't over-embellish. Next, I'll look for an online presence, such as a Github account and a personal website/blog. Bonus points for linking the accounts and websites in the resume.&lt;/p&gt;
&lt;p&gt;I'll browse the candidate's repos and blog posts, looking to get a feel for the candidate's experiences. When possible, I'll ask the candidate during the interview about personal projects or blog posts - a good way to build rapport and demonstrate interest.&lt;/p&gt;
&lt;p&gt;I don't expect anything excessive: a couple personal projects that are online or documented are sufficient. A website and a blog with a couple blog posts discussing their experiences is icing on the cake.&lt;/p&gt;
&lt;h3&gt;Technical competence&lt;/h3&gt;
&lt;p&gt;During the interview, I am looking for &lt;em&gt;competence&lt;/em&gt;, not necessarily &lt;em&gt;expertise&lt;/em&gt; or &lt;em&gt;excellence&lt;/em&gt;. That's not to say the bar isn't high, but that in a 1 hour interview there's only so much a candidate can do!&lt;/p&gt;
&lt;p&gt;To that end, I prefer to ask broad questions, i.e. toss softballs, hoping the candidate hits a home run. For example, if a candidate has used Django and Laravel, I could ask to compare the frameworks. A good answer would prove that the candidate has actually used the frameworks; a great answer will explain in-depth pain points or unique qualities of each. Open-ended questions are a chance for a candidate to demonstrate their experience, in a format more comfortable than trivia questions.&lt;/p&gt;
&lt;h2&gt;Red flags&lt;/h2&gt;
&lt;p&gt;Green flags wouldn't be complete without red flags: qualities that sink a candidate.&lt;/p&gt;
&lt;h3&gt;Poor communication&lt;/h3&gt;
&lt;p&gt;This is one of the first pieces of interview advice I learned, but it bears repeating because I've witnessed the mistake: communicate during the interview! If you're doing a coding exercise, explain your thought process, talk about the code that you're writing, ask questions - the interview is as much about understanding how you work as what you can produce. Even if you fail to complete the exercise, solid communication may score you ahead of a candidate who finished in silence.&lt;/p&gt;
&lt;h3&gt;Poor attitude&lt;/h3&gt;
&lt;p&gt;Similar to communication is attitude: be a good sport. Get into a good headspace before an interview by taking a walk or meditating. Advocate for yourself by taking breaks between sessions. Everyone has bad days - it may be better to reschedule than be grumpy.&lt;/p&gt;
&lt;p&gt;This isn't to suggest putting on faux-excitement over a job you're not excited about. You can show interest by asking good questions about the role and the company, and that's sufficient.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;These are just a few of my perspectives on interviewing, colored by experiences on both sides of the table.&lt;/p&gt;
&lt;p&gt;One key piece of advice: interviewing is a skill, practice makes perfect.&lt;/p&gt;</content><category term="career"></category></entry><entry><title>Remote Team Building</title><link href="https://blog.tmk.name/2024/03/24/remote-team-building/" rel="alternate"></link><published>2024-03-24T00:00:00-04:00</published><updated>2024-03-24T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-03-24:/2024/03/24/remote-team-building/</id><summary type="html">&lt;p&gt;I've had the privilege recently of organizing a few team building events. I am writing this to share my experience.&lt;/p&gt;
&lt;h2&gt;Welcome&lt;/h2&gt;
&lt;p&gt;The first event was to welcome a new team member. &lt;/p&gt;
&lt;p&gt;I chose Friday, as it's typically an easygoing day. In Google Calendar, I added the dozen team members, and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've had the privilege recently of organizing a few team building events. I am writing this to share my experience.&lt;/p&gt;
&lt;h2&gt;Welcome&lt;/h2&gt;
&lt;p&gt;The first event was to welcome a new team member. &lt;/p&gt;
&lt;p&gt;I chose Friday, as it's typically an easygoing day. In Google Calendar, I added the dozen team members, and stared at unavailability across the entire day. Thankfully, most meetings on Friday afternoons are 1 on 1s, easily rescheduled or rolled into the broader team social. I picked a time with few overlaps and sent the invite.&lt;/p&gt;
&lt;p&gt;I considered including an activity, versus letting conversation flow. I decided to include a game: with a remote meet of a dozen people, big personalities can dominate to the exclusion of others. A game provides the structure for everyone to participate.&lt;/p&gt;
&lt;p&gt;I searched Google and asked ChatGPT for help, finally settling on two truths and a lie. I didn't inform the team beforehand, to encourage spontaneous answers.&lt;/p&gt;
&lt;p&gt;The game went over very well, easily taking up over half an hour. I emcee'd, leading the votes for each round: "Raise your hand if you think XYZ is the lie."&lt;/p&gt;
&lt;p&gt;Throughout the game, people explained the stories behind the truths and the lies, and that naturally led to further conversation from the group.&lt;/p&gt;
&lt;h2&gt;Goodbye&lt;/h2&gt;
&lt;p&gt;The next event was saying goodbye to a team member. They weren't leaving on bad terms; they were well integrated and appreciated on the team.&lt;/p&gt;
&lt;p&gt;As before, I chose Friday afternoon - coinciding with the team member's departure.&lt;/p&gt;
&lt;p&gt;With invites sent, I faced the challenge of organizing a more somber event. After scouring the internet, I found a promising idea: each team member would share for a minute or so on their experience with, or advice for, the departing colleague.&lt;/p&gt;
&lt;p&gt;This went better than I expected. The emotion was palpable. The shares covered favorite memories, accomplishments, positive feedback, and advice. It was after this social that I came to appreciate the strongly positive team culture.&lt;/p&gt;
&lt;p&gt;Afterwards, I received private feedback from a team member asking me to organize the same event for them when they depart. I can't imagine more positive feedback.&lt;/p&gt;
&lt;h2&gt;And Welcome Again&lt;/h2&gt;
&lt;p&gt;Finally, I organized another welcome social for another new team member.&lt;/p&gt;
&lt;p&gt;Time slot: locked down on Friday afternoon.&lt;/p&gt;
&lt;p&gt;Game: out with two truths and a lie, in with a different guessing game. I wanted to switch things up, given how recently we had just played two truths and a lie. The game I settled on involved each team member sharing privately with me a fun fact or story, and during the social the team would guess whose story belonged to whom.&lt;/p&gt;
&lt;p&gt;There's a lot of startup effort involved in a game with setup like this. The team all shared their stories with me, but it took prodding.&lt;/p&gt;
&lt;p&gt;I put together a slideshow, with AI generated images for each story, using the story as a prompt.&lt;/p&gt;
&lt;p&gt;The game went well, but didn't last as long as I'd have hoped. To make it longer, I could've asked for multiple stories from each team member, or added in fake stories to the mix.&lt;/p&gt;
&lt;p&gt;The format of preparation did encourage different story telling than two truths and a lie. Some team members shared longer stories, others a simple fun fact.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In organizing just a few remote team social events, I have gained appreciation for event organizers and planners. I also appreciate my team's strongly positive culture, without which it's not hard for me to imagine suffering in awkward silences!&lt;/p&gt;
&lt;p&gt;I'm thankful for the opportunity to cut my teeth. Organizing is a powerful skill, small remote events being just the start.&lt;/p&gt;</content><category term="career"></category></entry><entry><title>Fantasies</title><link href="https://blog.tmk.name/2024/03/17/fantasies/" rel="alternate"></link><published>2024-03-17T00:00:00-04:00</published><updated>2024-03-17T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-03-17:/2024/03/17/fantasies/</id><summary type="html">&lt;p&gt;At the monastery yesterday, the monk explained how people form images of others in their minds, and those images crystallize. For example, older family members may treat you as if you were still a child, because in their mind they still &lt;em&gt;see you as a child&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Change and attachment&lt;/h2&gt;
&lt;p&gt;This …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At the monastery yesterday, the monk explained how people form images of others in their minds, and those images crystallize. For example, older family members may treat you as if you were still a child, because in their mind they still &lt;em&gt;see you as a child&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Change and attachment&lt;/h2&gt;
&lt;p&gt;This example is great, because time gives a concrete representation of &lt;em&gt;change&lt;/em&gt;. As people change, the images we have of them in our minds lag behind.&lt;/p&gt;
&lt;p&gt;We grow attached to the images, subconsciously, without intent. It takes constant effort to keep the images in sync with reality. This effort is rarely applied because people tend to change slowly.&lt;/p&gt;
&lt;p&gt;At a certain point, though, the gap between the image and reality may become too great, leading to disillusionment, a disturbance, conflict.&lt;/p&gt;
&lt;h2&gt;Conflict&lt;/h2&gt;
&lt;p&gt;This conflict is natural, and most of the time is resolved unconsciously. Sometimes, however, it bleeds into the conscious.&lt;/p&gt;
&lt;p&gt;One possible outcome is denial: pretend that the person hasn't changed, like the family members in the example. Denial is insidious and powerful, unconscious and invisible.&lt;/p&gt;
&lt;p&gt;Another outcome is confrontation: "You've changed!" spills out in the midst of a fight started by something unrelated. The confrontation expresses anger, in an attempt to pull the other person back into line with the image.&lt;/p&gt;
&lt;p&gt;Yet another outcome is acceptance: understand that one's perception is delusional, that one's mental images approximate but do not mirror reality. Accept that people change: impermanence.&lt;/p&gt;
&lt;h2&gt;How well do you know your friends?&lt;/h2&gt;
&lt;p&gt;This discussion is topical for me, as I am realizing that the images I have of people in my life are incongruent with reality. It's painful: I feel grief at the loss of the images, the fantasies. Loss is a perfectly apt description, even though I am not losing something &lt;em&gt;real&lt;/em&gt;: it's my attachment to the fantasies that causes suffering.&lt;/p&gt;
&lt;p&gt;I am grateful to have gained this perspective. In my past, I acted out, tried to coerce people to stay in line with my images. It never worked, though that didn't stop me from trying again repeatedly.&lt;/p&gt;
&lt;p&gt;Now I choose to accept that the people in my life are constantly changing, coming and going.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Lottery Ticket</title><link href="https://blog.tmk.name/2024/03/09/lottery-ticket/" rel="alternate"></link><published>2024-03-09T00:00:00-05:00</published><updated>2024-03-09T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-03-09:/2024/03/09/lottery-ticket/</id><summary type="html">&lt;p&gt;"You can't win the lottery without buying a ticket" - wisdom shared with me this week.&lt;/p&gt;
&lt;h2&gt;Buying a ticket&lt;/h2&gt;
&lt;p&gt;I have spent my adult life minimizing risk in favor of the safety of routine. I have bought few "lottery tickets," whether for small and large "jackpots." When I have, it's usually …&lt;/p&gt;</summary><content type="html">&lt;p&gt;"You can't win the lottery without buying a ticket" - wisdom shared with me this week.&lt;/p&gt;
&lt;h2&gt;Buying a ticket&lt;/h2&gt;
&lt;p&gt;I have spent my adult life minimizing risk in favor of the safety of routine. I have bought few "lottery tickets," whether for small and large "jackpots." When I have, it's usually at the behest of another, which allows me to mask my own vulnerability by projecting it onto another. Or, it's driven by the winds of fate, which absolves me of the vulnerability of making decisions.&lt;/p&gt;
&lt;p&gt;This has been the playbook by which I did not live my own life, but rather tagged along in others' lives, buffeted by forces and wills not my own. I avoided the risks involved in making decisions, taking chances: both &lt;a href="https://blog.tmk.name/2024/02/17/indecision/"&gt;failure and success&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In finding an emotionally safe and supportive community, I have developed the resilience necessary to experience my own vulnerability. This has enabled me to start taking chances. This has given me the courage to challenge myself, to risk not knowing myself, to chance changes to my life that might just get me where I want to be.&lt;/p&gt;
&lt;h2&gt;Winning the lottery&lt;/h2&gt;
&lt;p&gt;And where do I want to be? Only in digging deeply into my psychology have I been able to start to authentically ask and answer this question. "What do I want?" is so readily answered by my false self that seeks to &lt;em&gt;protect&lt;/em&gt; my vulnerability, guiding me towards the safety of the known and the routine.&lt;/p&gt;
&lt;p&gt;The work of uncovering my true self involves seizing opportunities to play the lottery. In playing, win or lose, I discover myself, I forge myself.&lt;/p&gt;
&lt;p&gt;Sure, it would be great for my wildest fantasies to come true. But so many of those fantasies are the stories that nourished and protected me when I wasn't willing to risk buying a ticket.&lt;/p&gt;
&lt;p&gt;As I start to buy tickets, I surrender my desire for any particular outcome. I accept the experience, the vulnerability, the uncertainty, just as it is. And that makes every ticket a winner.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>One Year of Fellowship</title><link href="https://blog.tmk.name/2024/03/02/one-year-of-fellowship/" rel="alternate"></link><published>2024-03-02T00:00:00-05:00</published><updated>2024-03-02T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-03-02:/2024/03/02/one-year-of-fellowship/</id><summary type="html">&lt;p&gt;I am approaching one year in a 12 step fellowship. As fate would have it, I also just wrapped up Step 12. While not easy to write about publicly, I want to take the opportunity to reflect on my experiences in the program.&lt;/p&gt;
&lt;h2&gt;Hitting a bottom&lt;/h2&gt;
&lt;p&gt;A nasty breakup led …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I am approaching one year in a 12 step fellowship. As fate would have it, I also just wrapped up Step 12. While not easy to write about publicly, I want to take the opportunity to reflect on my experiences in the program.&lt;/p&gt;
&lt;h2&gt;Hitting a bottom&lt;/h2&gt;
&lt;p&gt;A nasty breakup led me to the church basement. I was miserable, full of anger and resentment. I had just read a self-help book that suggested that my unhappiness was due to my own behavior, and in spite of heavy denial, I was open to try something new.&lt;/p&gt;
&lt;p&gt;I didn't even know what a "bottom" was at the time. A lot of terminology was new to me, as I started navigating the program. I felt awkward, running out the door the moment the meeting ended.&lt;/p&gt;
&lt;p&gt;In the shares and the readings I found a reason to keep coming back: my own story being shared by someone I've never met.&lt;/p&gt;
&lt;h2&gt;Fellowship&lt;/h2&gt;
&lt;p&gt;My sponsor told me that fellowship is half the program. It was months before I felt comfortable enough to hang around after meetings. I remember how anxious I felt when I first asked someone to meet for coffee. And how nervous I was when picking up the phone to call or text. The anxiety hasn't disappeared, but it's dissipated over time.&lt;/p&gt;
&lt;p&gt;My sponsor's words proved true: fellowship is indeed a critical, and awesome, part of the program. The relationships that I've developed with fellow travelers stand in stark contrast to those outside. There is an emotional safety that I haven't experienced elsewhere. There is so much power and healing, whether reaching out and being heard, or picking up the phone to lend a friendly ear.&lt;/p&gt;
&lt;h2&gt;Step work&lt;/h2&gt;
&lt;p&gt;I started the steps after about 6 months. I was nervous, and understandably so: I was intimidated by the prospect of amends, I didn't feel I had the spiritual "credentials." I managed to start despite the fears, and I am so happy that I did. If attending meetings is opening the door to a new life, the steps are walking through it and living it.&lt;/p&gt;
&lt;p&gt;I took my time, not aiming to rush through it. I called weekly with my sponsor to discuss the reading. I took breaks for vacation. I became willing to do what the program asked, on faith, and the guarantee - if it doesn't work, the program will refund my old life in full.&lt;/p&gt;
&lt;p&gt;I have begun making amends, which I believe are a reflection of my own growth and healing, as well as continued growth and healing. I will be honest, it isn't easy. It's required a ton of self reflection and humility. So far, I've noticed an enhanced peacefulness in my life as a direct result.&lt;/p&gt;
&lt;h2&gt;Balance sheet&lt;/h2&gt;
&lt;p&gt;So what has this all been for? I have spent a significant amount of time and energy on working the program, attending weekly meetings, engaging with fellowship.&lt;/p&gt;
&lt;p&gt;I have heard from others that I sound lighter now than when I walked in the door. They've noticed the changes, the growth, the healing within me.&lt;/p&gt;
&lt;p&gt;I have noticed it too. I have cultivated self-love, humility, honesty, and surrender. I try to keep my focus on myself, and let others live their own lives. I try to make space for others emotions. I try to feel and honor my own emotions. I try to ask for help when I need it.&lt;/p&gt;
&lt;p&gt;My relationships have changed, too. I have parted ways with close friends. I have a new awareness in relationships, and a sense of intention for selecting and nurturing positive relationships. Most importantly, my relationship with myself has improved dramatically, and that has improved all of my relationships.&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;p&gt;Finishing the steps isn't the end, but the beginning. I will continue to attend meetings, make inventories, and make amends. I can't wait to see what my life is like after another year in the program.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>The Only Harmless Great Thing</title><link href="https://blog.tmk.name/2024/02/24/the-only-harmless-great-thing/" rel="alternate"></link><published>2024-02-24T00:00:00-05:00</published><updated>2024-02-24T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-02-24:/2024/02/24/the-only-harmless-great-thing/</id><summary type="html">&lt;p&gt;For my science fiction book club I read this novelette (note: a novelette is shorter than a novella). In the meeting I was surprised to hear that a few members couldn't finish it. I &lt;em&gt;rarely&lt;/em&gt; stop reading a book - though there are countless unfinished books on my shelf.&lt;/p&gt;
&lt;p&gt;Anyway, to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;For my science fiction book club I read this novelette (note: a novelette is shorter than a novella). In the meeting I was surprised to hear that a few members couldn't finish it. I &lt;em&gt;rarely&lt;/em&gt; stop reading a book - though there are countless unfinished books on my shelf.&lt;/p&gt;
&lt;p&gt;Anyway, to the book. There are three plot lines told in parallel. The primary story is an alternate history where elephants are intelligent enough to communicate with humans. The elephants are brought in to US Radium to take over after the health disaster affecting humans. That plan is spoiled when one elephant kills the foreman, leading to her execution by electrocution (based on the true life story of Topsy the Elephant), which she spoils again through explosive revenge.&lt;/p&gt;
&lt;p&gt;The other two plotlines are closely linked thematically. One is the story of the racial unconscious of the elephants, who maintain their history by passing down stories from generation to generation. The other is a modern day scientist who devises a plan to make elephants glow near radioactive sites, in order to provide a warning for future generations about the danger. The author suggests that humanity lacks the memory of the elephants, which leads humanity to repeat its mistakes. Interestingly enough this is something I had been thinking of recently: the role that community plays in distributing knowledge, skills, stories. The book does not approach &lt;em&gt;why&lt;/em&gt; the memory of humanity is seemingly so short. Certainly, human communities build collective knowledge and stories and pass them down, I'm no historian but I'd presume in some cases for thousands of years or more. The loss of that community, its destruction, should be understood as a terrible tragedy, a result of catastrophe (war, famine, etc.). At the same time, it plays a rejuvenating role, leading to the creation of new communities, new stories, new memories.&lt;/p&gt;
&lt;p&gt;The author could have done a better job at explaining the real life history, because it's necessary context to appreciate the story. I had to read about US Radium and the "Radium Girls" who were lied to about the safety of ingesting radium. Fun but also disturbing fact: I live near the location of the old US Radium factory, and several Radium Girls are buried in a cemetary nearby. A member recalled the superfund site and excavation of mountains of contaminated dirt. Additionally, the real life electrocution of Topsy the Elephant. And a member mentioned a real life attempt by scientists to make cats glow near radiation.&lt;/p&gt;
&lt;p&gt;Overall I enjoyed the book, though it felt a bit trite, between the trope of a scientist's research being used for unintended and unethical purposes, and the orgy of violence in the finale.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>Indecision</title><link href="https://blog.tmk.name/2024/02/17/indecision/" rel="alternate"></link><published>2024-02-17T00:00:00-05:00</published><updated>2024-02-17T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-02-17:/2024/02/17/indecision/</id><summary type="html">&lt;p&gt;Indecision plagues me. I am planning a trip to a conference for my team, and have to pick the hotel. Thankfully, the conference recommends a few hotels. Not so thankfully, the options are mostly identical.&lt;/p&gt;
&lt;h2&gt;Why not choose?&lt;/h2&gt;
&lt;p&gt;Let's start at the beginning. I believe there's a spectrum of indecision …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Indecision plagues me. I am planning a trip to a conference for my team, and have to pick the hotel. Thankfully, the conference recommends a few hotels. Not so thankfully, the options are mostly identical.&lt;/p&gt;
&lt;h2&gt;Why not choose?&lt;/h2&gt;
&lt;p&gt;Let's start at the beginning. I believe there's a spectrum of indecision: in some scenarios, it's easier; for some people, it's easier. I currently lie towards the generally indecisive end: picking a restaurant, ordering household items on amazon, reaching out to someone, or writing a blog article. Rarely is the choice obvious.&lt;/p&gt;
&lt;p&gt;At the heart of indecision is fear. Admitting to fear is looked down upon, especially for a man, but a quote has stuck with me recently: courage is not the absence of fear, but acting despite the fear.&lt;/p&gt;
&lt;p&gt;Fear of failure is fear of making the wrong choice. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;If I buy the wrong household item, it'll break down and I'll feel like a fool&lt;/em&gt;. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;If I approach this stranger to talk, they may reject me.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Fear of success is fear of making the right choice. This can be difficult to appreciate. It's been described to me as: when the fear of making a change is greater than the fear of the status quo. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;If I book this trip, I'll have to give up the safety of my daily routines&lt;/em&gt;. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;If I try this new activity and find that I enjoy it, I may have to face that I don't know myself completely&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;How to choose&lt;/h2&gt;
&lt;p&gt;I read recently that it's widely believed that first comes confidence, then comes action - in reality, it's the opposite: first comes action, then comes confidence. So, how do I make choices? Let's look at a few tools.&lt;/p&gt;
&lt;h3&gt;Goals&lt;/h3&gt;
&lt;p&gt;Setting intentional goals is one way out of the impass. For example, with health and wellness as a goal, I will limit my intake of unhealthy food: this helps with choices at the grocery store as well as dining out.&lt;/p&gt;
&lt;p&gt;If frugality is the goal, then purchasing an item based on its cost effectiveness will guide my choice.&lt;/p&gt;
&lt;p&gt;Goals provide a metric for filtering and sorting: exclude the options that don't contribute to the goal, sort the remaining by how much each contributes.&lt;/p&gt;
&lt;h3&gt;Experts&lt;/h3&gt;
&lt;p&gt;Thanks to the internet, there's a review of practically everything under the sun: travel destinations, local car repair shops, and movies (cheers to watchmojo top 10 videos).&lt;/p&gt;
&lt;p&gt;This can, paradoxically, become overwhelming: attempting to read the review for dozens of choices does not help with decision paralysis, but contributes to it via increased stress.&lt;/p&gt;
&lt;p&gt;Expert guides, on the other hand, like wirecutter, are fantastic resources. This is another method I use for cutting down a broad field of choices to a select few.&lt;/p&gt;
&lt;h3&gt;Letting go&lt;/h3&gt;
&lt;p&gt;The above heuristics take a broad set of choices into a limited or ranked set of choices. Chances are, the final or top few will be roughly equivalent, just as in the conference hotels. I could spend untold hours pouring through reviews, endlessly sorting and re-sorting the list, but it won't make the choice for me.&lt;/p&gt;
&lt;p&gt;All that's left to do is let go. There's no certainty in life. There's no way to know the outcome ahead of time. The hotel choice may result in any number of possible horrors; or it may work out just fine. The choice really boils down to the paralysis of fear, or taking a leap of faith and deciding.&lt;/p&gt;
&lt;h2&gt;Embrace the choice&lt;/h2&gt;
&lt;p&gt;Reframing the experience of choosing from anxiety to empowerment can help to build a positive self-image and confidence. A life without choice would be dull and present no opportunity for self development. Embrace the chance to fail, to learn, to grow.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Travel Practical Tips</title><link href="https://blog.tmk.name/2024/02/10/travel-practical-tips/" rel="alternate"></link><published>2024-02-10T00:00:00-05:00</published><updated>2024-02-10T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-02-10:/2024/02/10/travel-practical-tips/</id><summary type="html">&lt;p&gt;As a follow up to my travel guidelines, I want to share some practical tips from my experience.&lt;/p&gt;
&lt;h2&gt;Research ahead of time&lt;/h2&gt;
&lt;p&gt;I'm paying a nice chunk of change to travel, so I will invest some time in planning ahead: tourist attractions, tourist pitfalls, etc.&lt;/p&gt;
&lt;p&gt;Reddit and youtube travel vlogs …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As a follow up to my travel guidelines, I want to share some practical tips from my experience.&lt;/p&gt;
&lt;h2&gt;Research ahead of time&lt;/h2&gt;
&lt;p&gt;I'm paying a nice chunk of change to travel, so I will invest some time in planning ahead: tourist attractions, tourist pitfalls, etc.&lt;/p&gt;
&lt;p&gt;Reddit and youtube travel vlogs are fantastic for this. I was so nervous about traveling to India that I even found a video of someone arriving at the airport and booking an uber to the hotel (warranted anxiety - it was overwhelming!).&lt;/p&gt;
&lt;h2&gt;Book but don't pay ahead&lt;/h2&gt;
&lt;p&gt;This bit me on a recent trip, where I booked the wrong dates for the hotel, and had pre-paid. Lesson learned!&lt;/p&gt;
&lt;h2&gt;Hotel breakfast&lt;/h2&gt;
&lt;p&gt;Be mindful of the price and reviews, to be sure, but in my experience, a decent hotel breakfast is a great way to start the day.&lt;/p&gt;
&lt;h2&gt;Travel esim&lt;/h2&gt;
&lt;p&gt;I was recently told about purchasing an esim for travel, and tried out Airalo on this trip. While pricier than purchasing a local sim card, it's also far less hassle. Landing from the flight with data access makes traveling to the hotel a breeze.&lt;/p&gt;
&lt;p&gt;And there's no risk of losing the sim card for home, since it never leaves the phone. Nice.&lt;/p&gt;
&lt;h2&gt;City mapper&lt;/h2&gt;
&lt;p&gt;City mapper is a directions app that specializes in certain cities. I found results to generally be higher quality than google maps. Compare and contrast - in some places, neither will be great.&lt;/p&gt;
&lt;h2&gt;Google maps list&lt;/h2&gt;
&lt;p&gt;This is a super handy feature: make a list for the trip, and add tourist attractions, restaurants, the hotel, etc.. This is great to help guide explorations around the city, and also serves as a historical list of places visited.&lt;/p&gt;
&lt;h2&gt;Look up&lt;/h2&gt;
&lt;p&gt;Speaking of map apps, I was amazed that in Amsterdam, there were restaurants and cafes missing! I learned my lesson: open my eyes and look around, don't tunnel vision to the internet.&lt;/p&gt;
&lt;h2&gt;Tourist transit passes&lt;/h2&gt;
&lt;p&gt;Some countries have info desks at metro stations - I asked at Amsterdam and was pleasantly surprised to find a great public transit pass (covering bus, tram, subway, train) geared towards tourists.&lt;/p&gt;
&lt;h2&gt;Go forth&lt;/h2&gt;
&lt;p&gt;This list is non-exhaustive - I will come up with more ideas as I continue to travel.&lt;/p&gt;</content><category term="travel"></category></entry><entry><title>Travel Guidelines</title><link href="https://blog.tmk.name/2024/02/03/travel-guidelines/" rel="alternate"></link><published>2024-02-03T00:00:00-05:00</published><updated>2024-02-03T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-02-03:/2024/02/03/travel-guidelines/</id><summary type="html">&lt;p&gt;I am traveling in Europe, and feeling inspired to reflect on some of my guidelines for travel. These are not hard and fast rules - they are guidelines, or philosophies.&lt;/p&gt;
&lt;h2&gt;Don't go back for seconds&lt;/h2&gt;
&lt;p&gt;In Barcelona there was a restaurant that served the most incredible hummus (in fact the restaurant …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I am traveling in Europe, and feeling inspired to reflect on some of my guidelines for travel. These are not hard and fast rules - they are guidelines, or philosophies.&lt;/p&gt;
&lt;h2&gt;Don't go back for seconds&lt;/h2&gt;
&lt;p&gt;In Barcelona there was a restaurant that served the most incredible hummus (in fact the restaurant was __called_ Hummus). I decided to go back a second time, and felt underwhelmed - it just wasn't as impressive the next time around. I don't fault the restaurant - the quality was there, but the novelty had faded.&lt;/p&gt;
&lt;p&gt;My takeaway is that it's best to make the effort to enjoy the experience fully just once. Be present, be engaged, and be bold.&lt;/p&gt;
&lt;p&gt;In future travels, I'd like to explore the "upgraded" experience: for example, choosing the tour at the museum.&lt;/p&gt;
&lt;h2&gt;Make plans, but leave room&lt;/h2&gt;
&lt;p&gt;It's recommended by some museums to book ahead, due to tickets selling out day-of. This encourages a rough structure for the day: for example, if the museum is at 2pm, then that may suggest brunch/lunch before, or stopping for a coffee/beer to relax after.&lt;/p&gt;
&lt;p&gt;I like to do some research before hand, making a google maps list of tourist attractions and restaurants. Local subreddits usually have a decent guide for tourists. I like to keep it loose, though, to leave room for spontaneous suggestions or invitations.&lt;/p&gt;
&lt;h2&gt;Break your routines&lt;/h2&gt;
&lt;p&gt;I don't want to recreate my home life somewhere else. Even this short trip, working part-time, has been a great opportunity to break free from the routines of daily home life.&lt;/p&gt;
&lt;p&gt;That includes eating habits, work habits, social habits - putting myself in an unfamiliar environment and adapting.&lt;/p&gt;
&lt;h2&gt;Push yourself but don't exhaust yourself&lt;/h2&gt;
&lt;p&gt;In Amsterdam on this trip, I landed from the red eye flight at 7am local time not having slept well.. and went to bed at 2am after a full day of adventure, and night of drinking. The next day I was so exhausted I barely left the hotel.&lt;/p&gt;
&lt;p&gt;I believe it's good to push myself on vacation - a fresh environment means old habits and self-imposed limits are lessened. I surprised myself already on this trip with how much energy I could muster.&lt;/p&gt;
&lt;p&gt;That said, I don't want to miss out due to exhaustion: I want to pace myself.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;I have taken five trips in the past year, and these are some of the lessons I've learned and plan to apply in the future.&lt;/p&gt;</content><category term="travel"></category></entry><entry><title>Django Database Performance Tips</title><link href="https://blog.tmk.name/2024/01/20/django-database-performance-tips/" rel="alternate"></link><published>2024-01-20T00:00:00-05:00</published><updated>2024-01-20T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-01-20:/2024/01/20/django-database-performance-tips/</id><summary type="html">&lt;p&gt;I've wanted to share this for some time now, but felt that it would be too large an undertaking. So I will keep it straightforward and avoid diving into metrics: I encourage you to test out the suggestions for your use cases.&lt;/p&gt;
&lt;p&gt;Note that I presume familiarity with web development …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've wanted to share this for some time now, but felt that it would be too large an undertaking. So I will keep it straightforward and avoid diving into metrics: I encourage you to test out the suggestions for your use cases.&lt;/p&gt;
&lt;p&gt;Note that I presume familiarity with web development and databases: think of this list as my step by step guide for diagnosing an issue, not a primer on database indices or schema design.&lt;/p&gt;
&lt;h2&gt;Diagnosing&lt;/h2&gt;
&lt;p&gt;To identify database performance issues, I will first try to use the django debug toolbar. At work, I even created a dummy view where I can plug in scripts and tasks (e.g. non-view-accessible code), just to use the debug toolbar.&lt;/p&gt;
&lt;p&gt;Sometimes, however, the overhead of the debug toolbar is too great, and a request won't finish in a reasonable amount of time (or in my lifetime!). In those cases, I use a context manager that prints out the SQL executed within. I use this within a jupyter notebook, which makes it easy to iterate.&lt;/p&gt;
&lt;p&gt;In either case, after this step I'll have pin-pointed the slowdown to a single slow query or an N+1 issue.&lt;/p&gt;
&lt;h2&gt;Profiling&lt;/h2&gt;
&lt;p&gt;In the case of a slow query, the next step is to check the query plan. I use a local copy of pev2, which can be &lt;a href="https://github.com/dalibo/pev2#all-in-one-local-no-installation-no-network"&gt;downloaded here&lt;/a&gt;. Follow the instructions to run the query with EXPLAIN - the exact SQL should be available from the previous step. I use dbeaver or pgadmin to interact directly with the database.&lt;/p&gt;
&lt;p&gt;Caveat: using EXPLAIN against a local database may not produce the same plan as in production. In my experience, ensuring that I have a similar amount of data helps, but if I can check in production, that's ideal.&lt;/p&gt;
&lt;h3&gt;Common Issues&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Missing index: may address full table scans.&lt;/li&gt;
&lt;li&gt;Too many joins: try prefetch related instead (see below).&lt;/li&gt;
&lt;li&gt;Slow join / subquery: experiment with subquery vs join.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes, the best solution is to just Write Another Query. It may feel awkward, and I'm sure there's some level of raw SQL-fu that could be performant in one query - but I stick to the ORM.&lt;/p&gt;
&lt;h2&gt;N+1&lt;/h2&gt;
&lt;p&gt;N+1 is the most common performance issue. To resolve, I'll reach for &lt;code&gt;select_related()&lt;/code&gt; and &lt;code&gt;prefetch_related()&lt;/code&gt;. Simple enough, most of the time, but beware &lt;code&gt;select_related()&lt;/code&gt;: adding joins can cause a query to slow to a crawl. In such circumstances, I will use &lt;code&gt;prefetch_related()&lt;/code&gt; instead - yes, even for a "*-to-one" relationship! A second query may be orders of magnitude faster than a join.&lt;/p&gt;
&lt;h2&gt;Rare: Avoid Loading More Than I Need&lt;/h2&gt;
&lt;p&gt;Sometimes, I'll find that the problem is that the query is just loading too much data. Think of what has to happen to get a field from the database to an object in Python: the data is read from disk by the database server, deserialized into memory, then serialized over the wire, then deserialized by the database driver in the webserver, then loaded into Django ORM objects: the cost adds up.&lt;/p&gt;
&lt;p&gt;Experimenting with &lt;code&gt;only()&lt;/code&gt; and &lt;code&gt;defer()&lt;/code&gt; can lead to surprising performance improvements.&lt;/p&gt;
&lt;p&gt;Caveat: &lt;code&gt;only()&lt;/code&gt; and &lt;code&gt;defer()&lt;/code&gt; are always maintenance burdens. Any field that's not initially included will require a new database query to fetch. I've seen it happen repeatedly: a field is added to the serializer but not to &lt;code&gt;only()&lt;/code&gt; (typically, they're defined in &lt;code&gt;serializers.py&lt;/code&gt; and &lt;code&gt;views.py&lt;/code&gt; respectively, so no locality of behavior to help!). Thus the performance boon becomes a performance penalty.&lt;/p&gt;
&lt;h2&gt;Rare: Don't pay the de/serialization cost&lt;/h2&gt;
&lt;p&gt;Suppose that I am trying to load a significant number of objects from the database. Scale is dependent, but let's say about 10k. The price of creating ORM objects is the difference between finishing the request in 30s or not (yes, still using Heroku).&lt;/p&gt;
&lt;p&gt;In such a case, dropping to &lt;code&gt;values()&lt;/code&gt; can be great - the queryset will return dictionaries with the specified properties. The performance gains can be substantial, even orders of magnitude.&lt;/p&gt;
&lt;p&gt;Caveat: &lt;code&gt;values()&lt;/code&gt; is only an option if I don't actually need model object instances.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Follow these steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Diagnose&lt;/li&gt;
&lt;li&gt;Profile&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then try the common solutions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add missing database indices&lt;/li&gt;
&lt;li&gt;Split the query into multiple&lt;/li&gt;
&lt;li&gt;&lt;code&gt;select_related()&lt;/code&gt;, &lt;code&gt;prefetch_related()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;only()&lt;/code&gt;, &lt;code&gt;defer()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;values()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="programming"></category></entry><entry><title>Senior</title><link href="https://blog.tmk.name/2024/01/13/senior/" rel="alternate"></link><published>2024-01-13T00:00:00-05:00</published><updated>2024-01-13T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-01-13:/2024/01/13/senior/</id><summary type="html">&lt;p&gt;I was recently promoted to &lt;em&gt;senior&lt;/em&gt; software engineer. Which warrants an examination of, what does it mean to be senior?&lt;/p&gt;
&lt;p&gt;Please note that this is a personal answer, based on my own experience - there is plenty of excellent literature on the subject. The key differentiators of seniority may change between …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I was recently promoted to &lt;em&gt;senior&lt;/em&gt; software engineer. Which warrants an examination of, what does it mean to be senior?&lt;/p&gt;
&lt;p&gt;Please note that this is a personal answer, based on my own experience - there is plenty of excellent literature on the subject. The key differentiators of seniority may change between individuals and companies, remote-first and non-remote cultures, etc.&lt;/p&gt;
&lt;h2&gt;Technical&lt;/h2&gt;
&lt;p&gt;Certainly, I expect a high technical bar for seniority. This doesn't necessarily mean in-depth knowledge of data structures, algorithms, system design, etc., etc. But it does mean a strong &lt;em&gt;breadth&lt;/em&gt; of knowledge, especially throughout the relevant domain of the company stack.&lt;/p&gt;
&lt;p&gt;It also means the capability to plan and execute on large scale projects. Note that scale doesn't always imply thousands of lines of code - in fact, I consider it an accomplishment to "thread the needle" on a complex, high impact, broad reaching project in a succint amount of code changes (though clever solutions can also be bad... as usual, it depends!).&lt;/p&gt;
&lt;p&gt;To be sure, I believe in continuing to develop my technical skills. But it's not the sole focus of a senior engineer: what's more important is that I feel confident that I could pick up a new technology when necessary.&lt;/p&gt;
&lt;h2&gt;Mentorship&lt;/h2&gt;
&lt;p&gt;Mentorship is an important part of seniority. At the moment, I invest time in regular 1 on 1s with other engineers, in particular new team members. Setting aside regular scheduled time creates an open floor for questions and rapport-building that can otherwise be awkward to initiate when working remotely.&lt;/p&gt;
&lt;p&gt;In addition, sharing resources for team education is an example of mentorship. For me, this includes articles, tutorials, and conference recordings, that I think would interest the team. Note it's easy to get carried away here and think that just posting links is enough - I think that demonstrations, for example "brown bag lunches", take education to the next level.&lt;/p&gt;
&lt;h2&gt;Leadership&lt;/h2&gt;
&lt;p&gt;With increased rank comes increased responsibility, and that responsibility will include coordinating within the team and across teams. A senior commands the respect of their peers and throughout the company: it's a subtle but valuable resource, to be used wisely.&lt;/p&gt;
&lt;p&gt;To be more concrete, a senior is expected to plan, propose and execute on projects, process improvements, etc. This involves coordinating others, striking compromise, and developing relationships.&lt;/p&gt;
&lt;p&gt;One final note that I think is important: leading by example. Other engineers will look at a senior engineer's behavior and emulate it: they set the bar, so make sure to set a high one. This comes into play for development (writing tests, writing clean code, responding well to PR feedback, etc.) as well as soft skills like communication and friendliness.&lt;/p&gt;
&lt;h2&gt;Business&lt;/h2&gt;
&lt;p&gt;Last but not least, seniority raises the bar for business impact. A senior may now have access to more information about business priorities; they are now expected to align their work with making an impact.&lt;/p&gt;
&lt;p&gt;This can be challenging, because it goes beyond the day to day work. Understanding business context means expanding my perspective to broader trends within the company and even beyond (e.g. the broader market). It means keeping my finger on the pulse of the business: growth, efficiency, and changing priorities.&lt;/p&gt;
&lt;p&gt;This is a skill under active development: I am susceptible to tunnel visioning into my current project (good for focus, not so great for managing changing priorities over time).&lt;/p&gt;
&lt;h2&gt;And beyond?&lt;/h2&gt;
&lt;p&gt;I look forward to developing these skills, seizing new and challenging opportunities. Next stop: &lt;em&gt;lead&lt;/em&gt; engineer 🚀&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Humility</title><link href="https://blog.tmk.name/2024/01/07/humility/" rel="alternate"></link><published>2024-01-07T00:00:00-05:00</published><updated>2024-01-07T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-01-07:/2024/01/07/humility/</id><summary type="html">&lt;p&gt;To me, humility means being teachable. That is, I don't have all the answers, I'm not immune to making mistakes.&lt;/p&gt;
&lt;p&gt;Recently it has helped me to pay attention when my instinct is to drift off: the impulse of "I know this already, there's nothing new for me."&lt;/p&gt;
&lt;p&gt;One instance is …&lt;/p&gt;</summary><content type="html">&lt;p&gt;To me, humility means being teachable. That is, I don't have all the answers, I'm not immune to making mistakes.&lt;/p&gt;
&lt;p&gt;Recently it has helped me to pay attention when my instinct is to drift off: the impulse of "I know this already, there's nothing new for me."&lt;/p&gt;
&lt;p&gt;One instance is at the monastery, when a layperson asked the monk about working through the loss of a loved one. I hadn't personally experienced this recently, so my instinct was to let my mind wander to something more interesting (what a cool movie I watched yesterday...). Instead, I paid attention, and it was highly rewarding. The monk explained that grief should be faced directly, i.e. not avoided. Be present with the feelings, understand the feelings, process and work through the feelings. As an example, the monk shared a story of someone who still grieved for their late spouse after a decade: certainly, this person was still working through the feelings, or possibly in denial about the need to finally move on and let go.&lt;/p&gt;
&lt;p&gt;I was surprised with how relevant the teaching was, despite my initial hesitation. I was not grieving the loss of a loved one, but I was grieving the loss of the "life I could have had" (e.g. missed opportunities, failures, so on). The same techniques and principles applied: face the feelings, be present with the feelings, understand the feelings, and process and work through the feelings. I want to work through my grief in order to let go and move on, as the grief will keep me anchored in the past, whereas I want to be firmly in the present.&lt;/p&gt;
&lt;p&gt;To be fair, I don't always find something relevant or interesting when I pay attention. Just as often I will struggle to concentrate during a meeting or lecture.&lt;/p&gt;
&lt;p&gt;But it ties into another idea I've been thinking over: setting the intention. When I try to pay attention in instances where I believe I have nothing to learn, I am setting the intention of humility: maybe I am wrong and I will learn something.&lt;/p&gt;
&lt;p&gt;With enough repetition, the intention becomes second nature. Humility, openness, "teachable"-ness, becomes second nature.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>You Don't Have to Do It All</title><link href="https://blog.tmk.name/2024/01/03/you-dont-have-to-do-it-all/" rel="alternate"></link><published>2024-01-03T00:00:00-05:00</published><updated>2024-01-03T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2024-01-03:/2024/01/03/you-dont-have-to-do-it-all/</id><summary type="html">&lt;p&gt;At work recently I helped lead an effort to solve a technical problem. I had been aware of this problem for some time, at least a month. However, I was hesitant to take action.&lt;/p&gt;
&lt;p&gt;Now that I'm on the other side, I feel I have a better understanding of my …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At work recently I helped lead an effort to solve a technical problem. I had been aware of this problem for some time, at least a month. However, I was hesitant to take action.&lt;/p&gt;
&lt;p&gt;Now that I'm on the other side, I feel I have a better understanding of my state of mind.&lt;/p&gt;
&lt;h2&gt;Getting the right people in the room&lt;/h2&gt;
&lt;p&gt;This problem required the coordination of several team mates. I took the initiative to put us all in a (virtual) room together, and we were able to make significant progress.&lt;/p&gt;
&lt;p&gt;After the technical progress, further coordination was required to wrap up. I took the initiative again and the problem was finally solved.&lt;/p&gt;
&lt;p&gt;In hindsight, I can see that the challenge was primarily coordination and not technical. I just had to get the right people in the room!&lt;/p&gt;
&lt;h2&gt;You don't have to do it all&lt;/h2&gt;
&lt;p&gt;Which gets to an understanding of my state of mind: because I didn't feel that I could solve the problem on my own, with my own technical skills, I hesitated to take action. The problem also went beyond my own typical responsibilities, which gave me pause.&lt;/p&gt;
&lt;p&gt;Certainly, others with the right skills, responsibilities, and context should step in. Right?&lt;/p&gt;
&lt;p&gt;Not so right! While I could not solve this on my own, I could play an important role in coordinating the team to solve the problem.&lt;/p&gt;
&lt;p&gt;This has been a great, albeit small, opportunity for leadership: I was not instrumental in solving the problem with my technical prowess, but rather in coordinating others.&lt;/p&gt;
&lt;h2&gt;Management is a skill&lt;/h2&gt;
&lt;p&gt;I feel good about my role in this effort. I have also identified areas of improvement when future leadership opportunities arise: recognize the leadership gap earlier, take bolder action, and communicate effectively.&lt;/p&gt;
&lt;p&gt;This small example has redoubled the importance of effective leadership, and the difference it can make to success - even in small problems.&lt;/p&gt;
&lt;p&gt;Management is a skill: some have natural talent, but most have to work to develop it. Do so - it makes an impact!&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Meditation Misconceptions</title><link href="https://blog.tmk.name/2023/12/31/meditation-misconceptions/" rel="alternate"></link><published>2023-12-31T00:00:00-05:00</published><updated>2023-12-31T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-12-31:/2023/12/31/meditation-misconceptions/</id><summary type="html">&lt;p&gt;This year I progressed my meditation skills from struggling to sit for three minutes to powering through forty five minutes. Here are some misconceptions that helped me to grow.&lt;/p&gt;
&lt;h2&gt;Meditation is easy&lt;/h2&gt;
&lt;p&gt;The idea is simple enough: sit down and do nothing. In practice, however, it was not easy for …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This year I progressed my meditation skills from struggling to sit for three minutes to powering through forty five minutes. Here are some misconceptions that helped me to grow.&lt;/p&gt;
&lt;h2&gt;Meditation is easy&lt;/h2&gt;
&lt;p&gt;The idea is simple enough: sit down and do nothing. In practice, however, it was not easy for me! Specifically, my mind would race with thoughts, I'd be uncomfortable, and I'd want to get up and do something - anything - else.&lt;/p&gt;
&lt;p&gt;It helped me to understand that meditation is not easy for most people, and that it's a practice, in which one trains the mind and body to sit comfortably, doing nothing, for an extended period of time.&lt;/p&gt;
&lt;h2&gt;Meditation requires a clear mind&lt;/h2&gt;
&lt;p&gt;I thought that doing meditation "correctly" meant having a clear mind (i.e. no thoughts) - yet my mind would race each time I sat down on the mat. What was wrong with me?&lt;/p&gt;
&lt;p&gt;Nothing! This was a big breakthrough for me in understanding meditation practice: thoughts will arise, one time, a dozen times, usually non-stop. This is just a fact of how I live my life: during the usual day, I am constantly engaged with thinking, so I bring that same activity to meditation.&lt;/p&gt;
&lt;p&gt;During meditation now, I try to accept the thoughts that arise, and then restore my attention to my breathing. It happens over and over and over again. And that's perfectly normal.&lt;/p&gt;
&lt;h2&gt;Meditation must be performed alone, in silence&lt;/h2&gt;
&lt;p&gt;This was how I tried meditation for years, and came up wanting. This year, I discovered guided meditation, and group meditation.&lt;/p&gt;
&lt;p&gt;Guided meditation has been highly rewarding: in particular, I like body scanning meditation, which moves the focus throughout the body. I also like metta (love and kindness) meditation.&lt;/p&gt;
&lt;p&gt;I also have found group meditation to be a powerful multiplier. I have performed my longest meditations in a group. I find it easier to stay focused and motivated this way.&lt;/p&gt;
&lt;h2&gt;There are good and bad meditations&lt;/h2&gt;
&lt;p&gt;As someone who tends towards self-criticism, this one plagued me: I'd sit down, my mind would race for the duration, the bell rings, and then I'd feel like I'd failed.&lt;/p&gt;
&lt;p&gt;The fact is that each time I sit down, I am setting an intention to clear my mind. I am purposefully clearing time for my mind and body to relax. This intention, and its demonstration, are powerful, positive practice.&lt;/p&gt;
&lt;p&gt;This means that there is no possibility of failure - showing up is not just half the battle!&lt;/p&gt;
&lt;h2&gt;Meditation must constantly increase in time&lt;/h2&gt;
&lt;p&gt;As someone who also tends towards pushing myself harder, I thought that I was expected to keep pushing my meditation duration longer and longer.&lt;/p&gt;
&lt;p&gt;This is not necessary! Progression will come naturally, in its own time. There is no need to run myself down, thinking I am not progressing fast enough.&lt;/p&gt;
&lt;p&gt;It's not a competition. As long as I show up consistently, and apply myself earnestly, I will make progress.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This is a short list of misconceptions that held me back from succeeding with meditation. As an early practictioner, I am sure that I will discover more as I continue my meditation journey.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>End of Year Review</title><link href="https://blog.tmk.name/2023/12/23/end-of-year-review/" rel="alternate"></link><published>2023-12-23T00:00:00-05:00</published><updated>2023-12-23T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-12-23:/2023/12/23/end-of-year-review/</id><summary type="html">&lt;p&gt;It's the end of the year, and a time for reflection.&lt;/p&gt;
&lt;h2&gt;Travel&lt;/h2&gt;
&lt;p&gt;In January I traveled to India with my ex-girlfriend, fulfilling a dream of mine. I visited New Delhi and Udaipur. I recall certain moments: the explosion of a transformer, children flying kites, vultures circling a dump, students competing …&lt;/p&gt;</summary><content type="html">&lt;p&gt;It's the end of the year, and a time for reflection.&lt;/p&gt;
&lt;h2&gt;Travel&lt;/h2&gt;
&lt;p&gt;In January I traveled to India with my ex-girlfriend, fulfilling a dream of mine. I visited New Delhi and Udaipur. I recall certain moments: the explosion of a transformer, children flying kites, vultures circling a dump, students competing in an inter-departmental sports event. I received about a dozen vaccines in preparation, and I managed to stay healthy until the plane home - and then I was sick for a few weeks.&lt;/p&gt;
&lt;p&gt;In July I visited Spain with my coworker. We met in Madrid, where we visited the Prado Museum and I discovered the works of Francisco Goya: I was awe-struck by the power and emotion in the paintings. My favorite was the dog. We took the train to Barcelona for a few days, and I dipped my toes into the Mediterranean for the first time. The humidity was oppressive, but the hummus at Hummus Barcelona was the best hummus I've ever had - and the patatas bravas were fantastic, too.&lt;/p&gt;
&lt;p&gt;In October I traveled to Los Angeles for work. I was put up in a nice hotel, walking distance to the Santa Monica beach. The weather was a perfect high-60s low-70s throughout. On the plane home, I was randomly sat next to a musician who collaborated with one of my coworkers - such a funny coincidence.&lt;/p&gt;
&lt;h2&gt;Books&lt;/h2&gt;
&lt;p&gt;In 2023 I read 25 books, the following of which stand out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster: The Acceleration of Just About Everything&lt;/li&gt;
&lt;li&gt;Codependent No More&lt;/li&gt;
&lt;li&gt;The Snow Leopard&lt;/li&gt;
&lt;li&gt;Zen Mind, Beginner's Mind&lt;/li&gt;
&lt;li&gt;A Journey in Ladakh&lt;/li&gt;
&lt;li&gt;Sheltered Lives&lt;/li&gt;
&lt;li&gt;Attached&lt;/li&gt;
&lt;li&gt;Player of Games&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Music&lt;/h2&gt;
&lt;p&gt;In my Spotify wrapped, Tycho topped the list - the first time Modest Mouse has been dethroned!&lt;/p&gt;
&lt;h2&gt;Work&lt;/h2&gt;
&lt;p&gt;I was promoted to Senior Software Engineer this year, marking a major milestone in my career.&lt;/p&gt;
&lt;p&gt;I successfully finished several key projects improving our internal logistics. My personal initiative to automate route creation was rolled out and is in active use.&lt;/p&gt;
&lt;h2&gt;Projects&lt;/h2&gt;
&lt;p&gt;Back in March and April I worked on a tower defense game, creatively titled Tower Defense Prototype. It was a lot of fun to build, and it challenged me to work on a system quite different than the usual; namely, using entity-component-systems as the architecture.&lt;/p&gt;
&lt;h2&gt;Personal development&lt;/h2&gt;
&lt;p&gt;After a breakup this year, one of the hosts on a podcast shared that breakups can be times of great creativity and positive change. While at the time it did not sooth my feelings, it did prove to be true. This breakup was the catalyst for transformative change.&lt;/p&gt;
&lt;p&gt;I attended improv acting classes for several months this year. This resolved a longstanding insecurity of mine: in high school, to impress my ex-girlfriend, I tried out for the school play, and did not get a part. In improv, I proved myself, and discovered the fun of acting, and the thrill of facing my fears.&lt;/p&gt;
&lt;p&gt;I started attending a monastery regularly for meditation. This has been invaluable. The sessions include group meditation, followed by dharma talks by the monks. Each monk has a distinct style and preferences for the meditation and talks: some are pragmatic, some are philosophical. In this practice I achieved my longest silent meditation of forty five minutes. An incredible feat considering I couldn't meditate for three minutes a year ago!&lt;/p&gt;
&lt;p&gt;Alongside my time at the moanstery, I started a daily meditation practice. This I have kept up with, according to Headspace, for almost 6 months, totaling 32 hours of meditation with the app!&lt;/p&gt;
&lt;p&gt;The most impactful change in my life has been attending a support group for adults coming from dysfunctional families. With this group, I have learned the importance of feeling my feelings, and keeping the focus on myself.&lt;/p&gt;
&lt;h2&gt;Next year&lt;/h2&gt;
&lt;p&gt;My resolutions for the next year include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Regular physical activity&lt;/li&gt;
&lt;li&gt;Reducing screen time&lt;/li&gt;
&lt;li&gt;Learning about finances and investing&lt;/li&gt;
&lt;li&gt;Continuing to nurture my physical health&lt;/li&gt;
&lt;li&gt;Continuing to nurture my mental health&lt;/li&gt;
&lt;li&gt;Continuing to nurture my spirituality&lt;/li&gt;
&lt;li&gt;Continuing to invest in friendships&lt;/li&gt;
&lt;li&gt;Continuing to practice meditation&lt;/li&gt;
&lt;li&gt;Continuing to eat well&lt;/li&gt;
&lt;li&gt;Continuing to sleep well&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It stands out to me that most of my resolutions are "continue to.." - which means that I don't have much new to do, mostly healthy habits to maintain.&lt;/p&gt;
&lt;p&gt;I have a few travel plans already scheduled, including attending FOSDEM and PyCon US. I want to make at least one other international trip, and one other domestic trip (outside of work).&lt;/p&gt;
&lt;p&gt;I am also weighing a move next year, whether local or not. I have a dream of moving abroad for a period of time.&lt;/p&gt;
&lt;h2&gt;The best year of my life&lt;/h2&gt;
&lt;p&gt;It's been a big year for me, the best of my life. Dreams fulfilled, immense strides in personal development. In many ways, I feel like not much has changed. But in key ways, I feel reborn.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Grace</title><link href="https://blog.tmk.name/2023/08/12/grace/" rel="alternate"></link><published>2023-08-12T00:00:00-04:00</published><updated>2023-08-12T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-08-12:/2023/08/12/grace/</id><summary type="html">&lt;p&gt;&lt;img alt="Give yourself some grace" src="https://blog.tmk.name/images/grace.png" /&gt;&lt;/p&gt;
&lt;p&gt;A recurring theme recently has been grace. I think of it as acceptance of my limitations, faults, mistakes, missteps, and more broadly "myself."&lt;/p&gt;
&lt;p&gt;I have a number of self-improvement efforts in motion at any given time. A recurring one is internet usage. While I work to be intentional with my …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="Give yourself some grace" src="https://blog.tmk.name/images/grace.png" /&gt;&lt;/p&gt;
&lt;p&gt;A recurring theme recently has been grace. I think of it as acceptance of my limitations, faults, mistakes, missteps, and more broadly "myself."&lt;/p&gt;
&lt;p&gt;I have a number of self-improvement efforts in motion at any given time. A recurring one is internet usage. While I work to be intentional with my computer usage, I am working on changing a habit that is almost two decades old. There are days where I choose to spend an excessive amount of time doomscrolling. I notice that these days coincide with strong or overwhelming emotions.&lt;/p&gt;
&lt;p&gt;Rather than treat myself poorly, for example with shame and guilt, I am learning to treat myself with grace. That means "giving myself permission" to spend time doomscrolling. After all, it's a very narrow focus that only looks at such behaviors without the bigger picture.&lt;/p&gt;
&lt;p&gt;I am proud of what I've accomplished, of how much I've grown. I am able to face complex, emotionally charged situations without panic, obsession, or compulsive reactions. I am taking charge of my life, taking responsibility for its outcomes. Two years ago I was miserable at a job, in which I could not handle maturely emotional difficulties. Recently I marked two years at a different job in which I've demonstrated emotional maturity and now reap the benefits: peace.&lt;/p&gt;
&lt;p&gt;To look at even an evening of doomscrolling and video games and shame myself is to miss the forest for the trees.&lt;/p&gt;
&lt;p&gt;I had the pleasure of connecting with an old colleague, and in a startup pitch he told me about his weight loss journey. He said that he'd tried the calorie counting apps, but felt that the constant awareness of intake and exercise led to feelings of shame. He then stumbled upon a different kind of app, which did not require calorie counting: instead, one took pictures of their meals and received gentle nudges with the goal of building healthier habits slowly.&lt;/p&gt;
&lt;p&gt;I am also a fairly habitual person by tendency, so I have a personal stake in new approaches. I think that positive reinforcement can be a powerful incentive. I also think that shame can be a powerful blocker to change: that's where grace comes in. Maybe there's an opportunity for a habit building app that works on positive reinforcement and grace.&lt;/p&gt;
&lt;p&gt;However, the interaction between grace and relapse can be tricky, especially for certain kinds of habits. Maybe that's the point of grace, though: if it were easy, it wouldn't be worth much!&lt;/p&gt;
&lt;p&gt;In a podcast recently I also heard an interesting take on addiction. Jung had told an alcoholic patient that recovery was rare, and typically required "spiritual realignment." I have been pondering this recently, and I think there is truth to it. Some habits are less intense, obsessive, compulsive, and easier to break - but others, true addictions, become so ingrained that transformation is necessary to overcome them. And even having overcome the behavior, doesn't mean the behavior disappears: it leaves its marks, it rears its head in dark times. Diligence is necessary to avoid relapse, though it does get easier over time - though it can be quite a long time to escape "survival mode."&lt;/p&gt;
&lt;p&gt;All that to say, I am practicing giving myself grace.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Spain</title><link href="https://blog.tmk.name/2023/07/30/spain/" rel="alternate"></link><published>2023-07-30T00:00:00-04:00</published><updated>2023-07-30T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-07-30:/2023/07/30/spain/</id><summary type="html">&lt;p&gt;In traveling some memories stick out, while others fade.&lt;/p&gt;
&lt;p&gt;In Udaipur, I recall the view from the hotel window. The window faced away from the city center. There was a small stream below, where I saw dogs, cows, hogs, horses, and birds. The stream was heavily polluted. Across the stream …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In traveling some memories stick out, while others fade.&lt;/p&gt;
&lt;p&gt;In Udaipur, I recall the view from the hotel window. The window faced away from the city center. There was a small stream below, where I saw dogs, cows, hogs, horses, and birds. The stream was heavily polluted. Across the stream ran a bridge, with nonstop traffic in both directions, and endless stream of mopeds and motorcycles and honking.&lt;/p&gt;
&lt;p&gt;One morning, while eating breakfast, I saw a transformer explode. Moments later the power cut out.&lt;/p&gt;
&lt;p&gt;At night, the city would grow dark: very few lights were visible. It's hard to appreciate the luxury of even a lighted room.&lt;/p&gt;
&lt;p&gt;During the day, young kids would fly kites from the roofs. Throughout the cityscape, kites flew.&lt;/p&gt;
&lt;p&gt;I traveled to Spain this month, visiting a colleague. I recall standing in the Mediterranean Sea for the first time, feeling the sand, the wind, the sun, the water washing over my feet.&lt;/p&gt;
&lt;p&gt;I recall small moments of excitement, like the first bite of fresh hummus and pita. The power cutting out in Barcelona first at a bar, and later at a sushi restaurant.&lt;/p&gt;
&lt;p&gt;I recall standing in the cold mountain stream outside Segovia. The humidity washing over me as we excited the train station in Barcelona.&lt;/p&gt;
&lt;p&gt;It's easier for me to transcribe these memories than to try to translate the experiences. I find I get tired of finding synonyms for incredible, fantastic, delicious. In someone else's hand these words could paint a picture, but in my own they fail to capture the souvenirs I've brought home.&lt;/p&gt;</content><category term="travel"></category></entry><entry><title>Jealousy</title><link href="https://blog.tmk.name/2023/07/07/jealousy/" rel="alternate"></link><published>2023-07-07T00:00:00-04:00</published><updated>2023-07-07T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-07-07:/2023/07/07/jealousy/</id><summary type="html">&lt;p&gt;On a podcast a psychologist explained how jealousy can help one find happiness. The idea is that, for those of us who struggle to discover our sources of happiness, we can re-frame what we're jealous of as potential sources of happiness.&lt;/p&gt;
&lt;p&gt;As an example, I am jealous of those who …&lt;/p&gt;</summary><content type="html">&lt;p&gt;On a podcast a psychologist explained how jealousy can help one find happiness. The idea is that, for those of us who struggle to discover our sources of happiness, we can re-frame what we're jealous of as potential sources of happiness.&lt;/p&gt;
&lt;p&gt;As an example, I am jealous of those who express themselves without hesitation. Though on writing that, I do appreciate that there is always some level of hesitation, some limit, some self-censorship. I suppose it's a scale of comfort in writing about personal topics. I am jealous of those who can write freely about their job, failures, successes, relationships, and so on. I hesitate strongly to write about myself, and hesitate even more strongly when thinking about writing about any one else.&lt;/p&gt;
&lt;p&gt;To put the idea to the test, I can think back to past jealousies.&lt;/p&gt;
&lt;p&gt;For example, I was jealous of those who meditated for long periods of time - where "long" meant more than 10 minutes. I have since done multiple 30 minute meditations. Would I say that I found a source of happiness, by engaging in a behavior I was jealous of in others? Perhaps, but I think there's something subtle about the process.&lt;/p&gt;
&lt;p&gt;It requires reflection to become aware that you have achieved a goal. Is it the achievement of the goal that brings happiness?&lt;/p&gt;
&lt;p&gt;I'm not so sure. Even as I write this, partially motivated by jealousy of others who write, I feel dissatisfied.&lt;/p&gt;
&lt;p&gt;Maybe the search for happiness starts from within. Okay, too cliche, I won't go there. Not exactly.&lt;/p&gt;
&lt;p&gt;Maybe it's just that the realization of the goal dispels the jealousy, leaving the goal for what it is. If it's something mundane, like buying some fancy new tech, once acquired the tech is just what it is, the jealousy has faded.&lt;/p&gt;
&lt;p&gt;Which suggests that jealousy can be a guide, but cannot decide what will make you happy.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Never Took the Time</title><link href="https://blog.tmk.name/2023/07/05/never-took-the-time/" rel="alternate"></link><published>2023-07-05T00:00:00-04:00</published><updated>2023-07-05T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-07-05:/2023/07/05/never-took-the-time/</id><summary type="html">&lt;p&gt;I recently read Four Thousand Weeks by Oliver Burkeman. One story that stood out to me was a conversation between the author and his neighbor. The author, seeing the neighbor repairing a lawnmower, complains that he wishes he were skilled at repairing things too. The neighbor replies, you aren't, because …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I recently read Four Thousand Weeks by Oliver Burkeman. One story that stood out to me was a conversation between the author and his neighbor. The author, seeing the neighbor repairing a lawnmower, complains that he wishes he were skilled at repairing things too. The neighbor replies, you aren't, because you never took the time to learn.&lt;/p&gt;
&lt;p&gt;This rings true to my childhood. Learning behavior is learned behavior. As a kid, my parents did not take the time to teach me. There was often discussion, as in one summer in which my stepmother said she'd teach me to cook: alas, I only remember heating cans of campbell's soup and boiling rice-in-a-bag (nice memory, A).&lt;/p&gt;
&lt;p&gt;Today, I often feel inadequate or insecure in comparison to others. Why don't I have musical talent? Where's my published book? Where's my AI side hustle?&lt;/p&gt;
&lt;p&gt;The answer to all such questions is, that I haven't taken the time to achieve these goals. It's a helpful and empowering reframing: the magic ingredients are time and effort. Thus, it comes down to choosing how I'd like to spend my time.&lt;/p&gt;
&lt;p&gt;And it's especially helpful when I feel like I am "wasting" a lot of time: playing video games, endless scrolling (is there a better name for this dark pattern that emphasizes the darkness [doom scrolling implies dread, rather than numbness...]?), and so on.&lt;/p&gt;
&lt;p&gt;In a given day, there's so many hours. I choose to spend those hours engaged in various activities. Aligning those activities with my goals is how I can "take the time" to become the person that I want to be.&lt;/p&gt;
&lt;p&gt;Mystery solved, eh?&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Shoulds</title><link href="https://blog.tmk.name/2023/07/04/shoulds/" rel="alternate"></link><published>2023-07-04T00:00:00-04:00</published><updated>2023-07-04T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-07-04:/2023/07/04/shoulds/</id><summary type="html">&lt;p&gt;A good friend once told me that he avoids the word "should" (as a disclaimer, he also claims I misremember what he says, so I may just be inspired by him, rather than quoting him).&lt;/p&gt;
&lt;p&gt;I was reminded of this today when listening to a podcast of a few New …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A good friend once told me that he avoids the word "should" (as a disclaimer, he also claims I misremember what he says, so I may just be inspired by him, rather than quoting him).&lt;/p&gt;
&lt;p&gt;I was reminded of this today when listening to a podcast of a few New York City comedians. One of them told a story about one of their recent guest appearances on a different podcast, in which they blew up in anger over vulgar talk about women.&lt;/p&gt;
&lt;p&gt;The comedian complained that the other hosts were talking about what they believe they &lt;em&gt;should&lt;/em&gt; talk about, in order to please their audience, and because that's what similar podcasts talk about. The young male audiences have been conditioned to expect vulgar talk about women, so that's what the podcast hosts talk about - despite, and I agree, this talk never amounting to action (I am being vague as I feel I &lt;em&gt;should&lt;/em&gt; keep my writing relatively free of vulgarity, see the note &lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; below).&lt;/p&gt;
&lt;p&gt;The drive, the pull, to do as I &lt;em&gt;should&lt;/em&gt;, has been a struggle for me, in pretty much every area of my life. It's a current that drags me to places that I don't want to go. It's a mountain that I force myself to climb, despite having no interest in reaching the summit.&lt;/p&gt;
&lt;p&gt;Grading well in school was a strong &lt;em&gt;should&lt;/em&gt;, and evolved easily into strong performance at work. As a consequence, I dropped out (more than once), and have had several nasty splits with work. I was so wrapped up in the need to excel, in the need to prove myself, that I pushed myself past blinking flashing beeping warning monitors until I burned out. &lt;/p&gt;
&lt;p&gt;I felt that I &lt;em&gt;should&lt;/em&gt; be right, all of the time, about everything, mistaking arrogance and condescension for confidence and wisdom.&lt;/p&gt;
&lt;p&gt;I felt that I &lt;em&gt;should&lt;/em&gt; write a certain way, with intelligence, wit, fancy words and grammar, letting these self-imposed rules limit me from writing anything at all.&lt;/p&gt;
&lt;p&gt;I felt that I &lt;em&gt;should&lt;/em&gt; take on more and more responsibilities until I collapse from the weight, because I'm only worth what I can do. It has taken me a long time to learn that I have worth regardless of my responsibilities and achievements.&lt;/p&gt;
&lt;p&gt;I felt that I &lt;em&gt;should&lt;/em&gt; put myself second, others first, mistaking codependence for empathy and altruism.&lt;/p&gt;
&lt;p&gt;I felt that I &lt;em&gt;should&lt;/em&gt; measure up against others who are more successful than me.&lt;/p&gt;
&lt;p&gt;I felt that I &lt;em&gt;should&lt;/em&gt; be able to achieve competency and excellence in any undertaking without appropriate training or consideration of my natural talents. This has multiple parts, one being the "expert beginner" syndrome, another patience and acceptance of the time it takes to practice.&lt;/p&gt;
&lt;p&gt;I could keep going. (&lt;em&gt;Should&lt;/em&gt; I keep going?)&lt;/p&gt;
&lt;p&gt;Shoulds cloud my perception. They are nasty, well dug in, hard to eliminate enemies. I liked the metaphor in the Four Agreements, that revealing sores to the sunlight is how they are cured. It can be painful to look at them clearly and directly, but healing is working through, no shortcuts.&lt;/p&gt;
&lt;p&gt;I don't have the solution to shoulds. I have heard that replacing &lt;em&gt;should&lt;/em&gt; with &lt;em&gt;want&lt;/em&gt; and &lt;em&gt;need&lt;/em&gt; is a good approach: in effect removing the judgment from outside and listening to what one feels inside.&lt;/p&gt;
&lt;p&gt;It's an active process to work through, and I find that &lt;em&gt;should&lt;/em&gt; will sneak up when I am not guarded. But I have noticed that it's gotten easier, that I feel lighter and less stressed, less judgmental, about what I &lt;em&gt;should&lt;/em&gt; be doing, and more in tune with what I &lt;em&gt;want&lt;/em&gt; to be doing. &lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Episode 122 of the &lt;a href="https://www.patreon.com/OutForSmokes/posts"&gt;Out for Smokes podcast&lt;/a&gt;&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="reflections"></category></entry><entry><title>Noticed</title><link href="https://blog.tmk.name/2023/07/03/noticed/" rel="alternate"></link><published>2023-07-03T00:00:00-04:00</published><updated>2023-07-03T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-07-03:/2023/07/03/noticed/</id><summary type="html">&lt;p&gt;In the woods I noticed the crunch of my sandal on the rocky path. I listened to the wind rustle the leaves. I observed the proud posture of the robin. I appreciated the nobility of the lone tree. I witnessed the decay, the rebirth.&lt;/p&gt;
&lt;p&gt;I startled a blue jay, who …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In the woods I noticed the crunch of my sandal on the rocky path. I listened to the wind rustle the leaves. I observed the proud posture of the robin. I appreciated the nobility of the lone tree. I witnessed the decay, the rebirth.&lt;/p&gt;
&lt;p&gt;I startled a blue jay, who darted off. And I was watched by a deer with fuzzy antlers.&lt;/p&gt;
&lt;p&gt;I find that these small moments of presence give birth to strong memories. I am apt to drift in distraction; these moments possess some strange spell.&lt;/p&gt;
&lt;p&gt;The ring of my water bottle bouncing against my leg sounded like a prayer bowl, calling me forth to the present moment.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>The Snow Leopard</title><link href="https://blog.tmk.name/2023/07/02/the-snow-leopard/" rel="alternate"></link><published>2023-07-02T00:00:00-04:00</published><updated>2023-07-02T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-07-02:/2023/07/02/the-snow-leopard/</id><summary type="html">&lt;blockquote&gt;
&lt;p&gt;With the wind and cold, a restlessness has come, and I find myself hoarding my last chocolate for the journey back across the mountains - forever getting-ready-for-life instead of living it each day.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This passage, from The Snow Leopard by Peter Matthiessen, reverberates within me.&lt;/p&gt;
&lt;p&gt;I find myself writing not what …&lt;/p&gt;</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;With the wind and cold, a restlessness has come, and I find myself hoarding my last chocolate for the journey back across the mountains - forever getting-ready-for-life instead of living it each day.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This passage, from The Snow Leopard by Peter Matthiessen, reverberates within me.&lt;/p&gt;
&lt;p&gt;I find myself writing not what I want to write, not what flows through me naturally in conversation with a good friend or colleague. I write what I think I should write, I let the accumulated weight of tradition inhibit me.&lt;/p&gt;
&lt;p&gt;And when I do write, I steel myself - as it's necessary to do - by promising myself that I am preparing myself for future writings.&lt;/p&gt;
&lt;p&gt;I agree with the author of The Four Agreements, that I live in a dream world, a world in which I limit myself by my beliefs.&lt;/p&gt;
&lt;p&gt;I wonder if this why I also struggle to communicate freely with some people, and not others. I believe there's an image I have to create and maintain of myself, a mirage to impress others. As I have learned, others will see me through their own distorted vision, as I see them through mine.&lt;/p&gt;
&lt;p&gt;I am attracted to the Buddhism that teaches: when you see, you see; when you hear, you hear; when you taste, you taste; when you feel, you feel; when you smell, you smell. I don't understand how straightforward teaching can lead to splintering.&lt;/p&gt;
&lt;p&gt;I struggle with such basic undertakings. It is shameful, really. But I am coming to terms with shame and guilt and fear: they are indicators, they are guides, but they are not the truth, they are not me. My truth is that I am here and now, writing my truth.&lt;/p&gt;
&lt;p&gt;Fear turns me inside out, so that instead of focusing on myself, I am focusing on the outside world. Someone recently related it to control, which I related to obsessive worry. I spent two weeks obsessively worrying about a parking spot. I sought reassurance from multiple sources, and it could not still my - mind? body? from where does the fear arise?&lt;/p&gt;
&lt;p&gt;I do have a theory about this. I relate it to insecurity. A fundamental insecurity. At the innermost core of my identity, of me, lies something fractured. I picture it as a black sphere held up by pillars and scaffolding and various impromptu support structures. A secure person has a secure foundation. An insecure person has an insecure foundation: pillars are broken or missing, and replacing them may be toxic and unhealthy supports, like drugs, maladaptations (e.g. codependency, numbness).&lt;/p&gt;
&lt;p&gt;The pillars that support a person's fundamental security change throughout time, but early scars, especially early childhood scars, may linger unnoticed for decades.&lt;/p&gt;
&lt;p&gt;Tying it back together, I believe it's this insecurity that hinders me from mindfulness. I am forever getting-ready-for-life in order to steel myself, to build up enough support, that I believe I need in order to survive.&lt;/p&gt;
&lt;p&gt;I also believe, or am coming to believe, that I can support myself, that the pillars of support are meant to fade away, not to be replaced.&lt;/p&gt;</content><category term="books"></category></entry><entry><title>Immediacy</title><link href="https://blog.tmk.name/2023/02/21/immediacy/" rel="alternate"></link><published>2023-02-21T00:00:00-05:00</published><updated>2023-02-21T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-02-21:/2023/02/21/immediacy/</id><summary type="html">&lt;p&gt;A bias that I have come to notice in myself is the immediacy bias. This is bias towards reacting immediately to a stimulus, without consideration for the context or big picture.&lt;/p&gt;
&lt;p&gt;A software example is the bug report. This may come in a raw form, like a new error in …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A bias that I have come to notice in myself is the immediacy bias. This is bias towards reacting immediately to a stimulus, without consideration for the context or big picture.&lt;/p&gt;
&lt;p&gt;A software example is the bug report. This may come in a raw form, like a new error in Sentry, or a Slack message from an internal user, or as a customer review on the App Store. I notice that I am drawn towards investigating and fixing such issues immediately. Seemingly noble (who doesn't want to be the one who fixed a critical bug quickly?), this has significant drawbacks: certainly context switching away from active work, but even more importantly the opportunity cost.&lt;/p&gt;
&lt;p&gt;Triaging is the process of assigning severity to the bug, for which there are predefined standard response procedures. This is a remedy for the immediacy bias. It provides a helpful framework in a stressful situation and clearly specifies when immediacy is necessary and when it is not.&lt;/p&gt;
&lt;p&gt;In my experience, only mission critical bugs require an immediate response. This should be laid out in the standard operating procedures, for the assistance of the engineer, but also to build it into the company wide culture. If a bug is reported publicly, and management panics and demands action, that reinforces a bad dynamic within the company.&lt;/p&gt;
&lt;p&gt;(Similarly, Kevin Mitnick in The Art of Deception explains that management must abide by the same security protocols as everyone else, i.e. leading from above. I just finished the book - excellent and enjoyable read.)&lt;/p&gt;
&lt;p&gt;Without triaging, or with bad triaging practices (i.e. there's pressure to investigate and fix bugs quickly), engineers are taught to independently make decisions in-the-moment, with the full brunt of the immediacy bias at play. It's not easy to ignore a customer's complaint, or a new Sentry error, but sometimes it's necessary.&lt;/p&gt;
&lt;p&gt;As for the costs, context switching from active work leaves both jobs poorly distributed in the mind's resource: attention. There's a cost to entering and exiting focus. On top of this, the pressure of the immediacy bias will leave the bug front and center in the mind until it's resolved, but that typically takes hours, if not days: investigation, fix, deploy, follow up, and urgent communications.&lt;/p&gt;
&lt;p&gt;The opportunity cost ties into context switching, as there will be decreased performance until the bug is resolved. But at a higher level the time spent on the bug could have been put productively to use in one's existing work. Investing blocks of time to focus on a project is critical for efficient success. In Paul Graham's essay on the Maker and Manager Schedules, he explains that for the Maker (i.e. the programmer), large chunks of time must be set aside for creative work.&lt;/p&gt;
&lt;p&gt;While the above focuses on bugs, the immediacy bias applies to all types of decisions.&lt;/p&gt;
&lt;p&gt;Another example that I find pressing is (push) notifications. Slack messages, emails, my phone, etc., all demand immediate response, and at significant cost. The tools developed to combat this shed light on how we should structure our responses in other instances.&lt;/p&gt;
&lt;p&gt;One tool is "Do Not Disturb" mode, which I have enabled at work. This prevents notifications from flashing and making sound in order to steal my attention.&lt;/p&gt;
&lt;p&gt;Another example is notification settings, i.e. specifying which applications are allowed to send which kinds of messages. The filtering is a great feature on Android, but unfortunately I've noticed that some application developers do not label the notification types, meaning it's all or nothing: either you accept the spammy marketing notifications or you don't get the useful notifications.&lt;/p&gt;
&lt;p&gt;I recently reviewed my notification settings with a critical eye. One notable example is email: why do I need to have personal email notifications? I can't think of the last time that a notification improved my life over checking my email in the morning and evening.&lt;/p&gt;
&lt;p&gt;This can paradoxically lead to greater attention lost if not approached thoughtfully. For example, while I enable "Do Not Disturb" at work, I now spend time frequently checking my email, whenever I see the tab notification badge. The same is true for Slack, except globally on my monitor with the icon in the menu bar.&lt;/p&gt;
&lt;p&gt;That aside, the efficacy of these techniques demonstrates that immediate response is rarely warranted. When making decisions in life, one should not feel compelled to immediate action: take the time you need to calmly think through the matter at hand, taking into account the context.&lt;/p&gt;
&lt;p&gt;I am beginning to appreciate just how precious is my attention. The immediacy bias is an important enemy to understand. It's critical to develop defensive techniques to safeguard one's attention.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Stimulation</title><link href="https://blog.tmk.name/2023/02/14/stimulation/" rel="alternate"></link><published>2023-02-14T00:00:00-05:00</published><updated>2023-02-14T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-02-14:/2023/02/14/stimulation/</id><summary type="html">&lt;p&gt;Hi, my name is Tristan and I am a stimulation addict. How did this come to happen?&lt;/p&gt;
&lt;p&gt;I recently finished Faster by James Gleick. The narrative was fairly chaotic (hehe) and unfocused, but the truth is I failed to understand the lesson until it was made explicit: we've lost the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hi, my name is Tristan and I am a stimulation addict. How did this come to happen?&lt;/p&gt;
&lt;p&gt;I recently finished Faster by James Gleick. The narrative was fairly chaotic (hehe) and unfocused, but the truth is I failed to understand the lesson until it was made explicit: we've lost the pauses in daily life.&lt;/p&gt;
&lt;p&gt;Perhaps this is because the ideology of our time has made this intensification invisible to me. It reminds me of how Zizek described the sunglasses in They Live: they must be put on in order to see the truth, as opposed to something that must be removed (blinders, the pill from Equilibrium, etc.). In order to see what was happening in front of my face, I needed to learn to see it.&lt;/p&gt;
&lt;p&gt;In the past, there was what we now call "free time" or what used to be called "leisure time," in other words time spent doing nothing. Today, we long for free time: so where did it all go?&lt;/p&gt;
&lt;p&gt;The availability of endlessly stimulating activities for cheap chemically reacted with the relentless human craving for stimulation, to produce an in-hindsight all-to-easy-to-not-foresee future in which we're all desperate to reduce our stimulation while continuing to see just how much stimulation we can possibly experience.&lt;/p&gt;
&lt;p&gt;Considering the drug addict, the current horror of TikTok is nowhere near the end of the line. Stimulation will continue to develop until at par or superior to (or probably in coincidence with) drugs.&lt;/p&gt;
&lt;p&gt;I'm reminded of The Three Stigmata of Palmer Eldritch, where drugs and toys are used together to produce an experience more stimulating than the misery of life in the colonies. The plot here may be prophetic (give PKD some credit..): Can-D is about to be outcompeted by the even more stimulating drug Chew-Z. The development towards increasing stimulation marches onwards.&lt;/p&gt;
&lt;p&gt;As for me, I struggle with idleness. I often think to myself, I wish I could pick up a new hobby, but where would I find the time for it? And yet at the same time, looking at the results of a day gone by, I find that much time has been "wasted" in stimulating but "nutrition-less" activities. Junk activities. Empty activities. High on information.&lt;/p&gt;
&lt;p&gt;I needn't feel alone: according to a study in the book, people when asked about an average day report reading for 30 minutes a day, but when asked about yesterday, respond that they did not read at all. Where did the time go?&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Django Model Bulk Loading Slowness Pt. 2</title><link href="https://blog.tmk.name/2023/01/07/django-model-deserialization-slowness-pt-2/" rel="alternate"></link><published>2023-01-07T00:00:00-05:00</published><updated>2023-01-07T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-01-07:/2023/01/07/django-model-deserialization-slowness-pt-2/</id><summary type="html">&lt;p&gt;It turns out that the django-model-changes library had been forked and modified some time ago by engineers at the company. Among the changes was removing the dynamic signal attachments from the &lt;code&gt;ChangesMixin&lt;/code&gt; &lt;code&gt;__init__&lt;/code&gt; method:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;__init__&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;*&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;args,&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;**&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;kwargs):&lt;/span&gt;
    &lt;span style="color: #FFD173"&gt;super&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(ChangesMixin,&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #FFD173"&gt;__init__&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;*&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;args,&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;**&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;kwargs)&lt;/span&gt;

    &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;_states&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;[]&lt;/span&gt;
    &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;_save_state …&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;It turns out that the django-model-changes library had been forked and modified some time ago by engineers at the company. Among the changes was removing the dynamic signal attachments from the &lt;code&gt;ChangesMixin&lt;/code&gt; &lt;code&gt;__init__&lt;/code&gt; method:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #FFAD66"&gt;def&lt;/span&gt; &lt;span style="color: #FFD173"&gt;__init__&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;,&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;*&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;args,&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;**&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;kwargs):&lt;/span&gt;
    &lt;span style="color: #FFD173"&gt;super&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(ChangesMixin,&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #FFD173"&gt;__init__&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;*&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;args,&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;**&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;kwargs)&lt;/span&gt;

    &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;_states&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;=&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;[]&lt;/span&gt;
    &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;_save_state(new_instance&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=True&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;

    &lt;span style="color: #d4d2c8"&gt;signals&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;post_save&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;connect(&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;_post_save,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;sender&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;__class__,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;dispatch_uid&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;django-changes-&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;%s&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;%&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;__class__&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;__name__&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;signals&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;post_delete&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;connect(&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;_post_delete,&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;sender&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;__class__,&lt;/span&gt;
        &lt;span style="color: #d4d2c8"&gt;dispatch_uid&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;=&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;django-changes-&lt;/span&gt;&lt;span style="color: #95E6CB"&gt;%s&lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;#39;&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;%&lt;/span&gt; &lt;span style="color: #5CCFE6"&gt;self&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;__class__&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;__name__&lt;/span&gt;
    &lt;span style="color: #d4d2c8"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By just commenting out the signals code, the improvement is dramatic:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Python&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span style="color: #d4d2c8"&gt;In&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;[&lt;/span&gt;&lt;span style="color: #DFBFFF"&gt;1&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;]:&lt;/span&gt; &lt;span style="color: #FFAD66"&gt;%&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;timeit&lt;/span&gt; &lt;span style="color: #FFD173"&gt;list&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;(Dese&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;objects&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;all())&lt;/span&gt;
&lt;span style="color: #DFBFFF"&gt;841&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;ms&lt;/span&gt; &lt;span style="color: #f88f7f"&gt;±&lt;/span&gt; &lt;span style="color: #DFBFFF"&gt;3.61&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;ms&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;per&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;loop&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;(mean&lt;/span&gt; &lt;span style="color: #f88f7f"&gt;±&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;std&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;dev&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;.&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;of&lt;/span&gt; &lt;span style="color: #DFBFFF"&gt;7&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;runs,&lt;/span&gt; &lt;span style="color: #DFBFFF"&gt;1&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;loop&lt;/span&gt; &lt;span style="color: #d4d2c8"&gt;each)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This brings the slowdown to around 1.7x, still significant but not nearly as brutal as previously thought. It still leaves the question - why do I see such an immense slowdown at work?&lt;/p&gt;
&lt;p&gt;One other thought is that the models are simply huge. I cannot overstate the model obesity epidemic: the BMI is off the charts, thousands upon thousands of lines of code for a single model. Let's explore adding many more properties to the model, to see if anything interesting happens.&lt;/p&gt;
&lt;p&gt;Adding 26 &lt;code&gt;TextField&lt;/code&gt; properties to our model, let's check the speed:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [1]: %timeit list(Dese.objects.all())
6.85 s ± 218 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Quite the slowdown, but possibly due to the content of the &lt;code&gt;TextFields&lt;/code&gt;. Let's only return the object primary keys:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [2]: %timeit list(Dese.objects.all().only(&amp;#39;pk&amp;#39;))
1.81 s ± 24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Aha, adding more model properties does slow down the model loading. Compared to without the &lt;code&gt;ChangesMixin&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [1]: %timeit list(Dese.objects.all().only(&amp;#39;pk&amp;#39;))
700 ms ± 10.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's a significant difference at 2.5x. Upon further investigation, it becomes clear that the issue is due to the implementation of the &lt;code&gt;ChangesMixin&lt;/code&gt;. Upon object creation, every model property is copied into a dictionary. This makes sense - the purpose of the library is to enable figuring out what has changed over an object's lifetime.&lt;/p&gt;
&lt;p&gt;However, the implementation is very slow, as it reads and copies properties individually from &lt;code&gt;self.__dict__&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;class ChangesMixin:
    def current_state(self):
    local_data = self.__dict__

        fields = {}

        for field in self._meta.local_fields:

            fields[field.name] = local_data.get(field.name)
            fields[field.attname] = local_data.get(field.attname)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;basic implementation outlineSome brief internet sleuthing revealed that an &lt;a href="https://bugs.python.org/issue31179"&gt;update&lt;/a&gt; to &lt;code&gt;dict.copy()&lt;/code&gt; has vastly improved performance over other approaches. Let's see what happens when we modify the above code:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [1]: %timeit list(Dese.objects.all().only(&amp;#39;pk&amp;#39;))
844 ms ± 11.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Wow, not bad! We recovered roughly half of the performance loss. Let's rack it up a notch and add more properties to the model. Adding 26 more fields to the model, we find:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [1]: %timeit list(Dese.objects.all().only(&amp;#39;pk&amp;#39;))
1.11 s ± 15.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;An incremental slowdown from fewer fields. Compared to the naive copy implementation:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [1]: %timeit list(Dese.objects.all().only(&amp;#39;pk&amp;#39;))
2.93 s ± 15.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We see even more improvement than at fewer fields - which makes sense to me.&lt;/p&gt;
&lt;p&gt;Finally, compared to no &lt;code&gt;ChangesMixin&lt;/code&gt; at all:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [1]: %timeit list(Dese.objects.all().only(&amp;#39;pk&amp;#39;))
974 ms ± 24.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We see that the improved copy implementation continues to shine.&lt;/p&gt;
&lt;p&gt;Notably to me there is a slowdown that occurs even at the base Django model as more fields are added. While I may not expect it to be lightning fast, I wouldn't expect almost 50% loss in performance by roughly doubling the number of fields.&lt;/p&gt;
&lt;p&gt;I wonder if there is a more efficient way to load Django model objects (i.e. full Django model objects, not just dictionaries). But then again I also appreciate that this is not necessarily the goal of the project - magic always comes at a cost :)&lt;/p&gt;
&lt;h2&gt;Addendum&lt;/h2&gt;
&lt;p&gt;The proposed improvement to &lt;code&gt;ChangesMixin&lt;/code&gt; would require additional effort to fully realize. The &lt;code&gt;changes()&lt;/code&gt; dictionary which is at least our own primary use case would need to iterate over the fields of the model, constructing the diff between the &lt;code&gt;__dict__&lt;/code&gt; copies. However I don't think this would cause necessarily any loss of performance on that front either, and may actually be an improvement, as I believe that defered fields may be inappropriately marked changed in the current implementation.&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Django Model Bulk Load Slowness</title><link href="https://blog.tmk.name/2023/01/04/django-model-deserialization-slowness/" rel="alternate"></link><published>2023-01-04T00:00:00-05:00</published><updated>2023-01-04T00:00:00-05:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2023-01-04:/2023/01/04/django-model-deserialization-slowness/</id><summary type="html">&lt;p&gt;At work, we have a database export process that transfers data from our OLTP PostgreSQL database to our OLAP RedShift database. This export is exceedingly slow, causing headaches for the engineers, as deployments are typically delayed until the export finishes.&lt;/p&gt;
&lt;p&gt;In diving into the export process, I discovered that one …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At work, we have a database export process that transfers data from our OLTP PostgreSQL database to our OLAP RedShift database. This export is exceedingly slow, causing headaches for the engineers, as deployments are typically delayed until the export finishes.&lt;/p&gt;
&lt;p&gt;In diving into the export process, I discovered that one major factor is the price of model object deserialization - in other words, the translation of the SQL query response into Django model objects.&lt;/p&gt;
&lt;p&gt;Compared to returning plain tuples with &lt;code&gt;values_list()&lt;/code&gt;, the construction of the model objects can be 800x slower.&lt;/p&gt;
&lt;p&gt;Let's explore this, and also explore code snippets in Ghost.&lt;/p&gt;
&lt;p&gt;With 100,000 objects in the creatively named &lt;code&gt;Dese&lt;/code&gt; table (dese-rialize, hehe), let's compare. First, a look at the model:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;class Dese(models.Model):
    content = models.TextField()
    created_at = models.DateTimeField()
    is_active = models.BooleanField()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, let's compare default object deserialization with &lt;code&gt;values_list()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [6]: %timeit list(Dese.objects.all())
493 ms ± 20.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [8]: %timeit list(Dese.objects.all().values_list(&amp;#39;id&amp;#39;, &amp;#39;content&amp;#39;, &amp;#39;created_at&amp;#39;, &amp;#39;is_active&amp;#39;))
151 ms ± 3.82 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As we can see, it's about 3x faster to use &lt;code&gt;values_list()&lt;/code&gt;. However, this doesn't quite reach the slowness I witnessed at work. What else could it be?&lt;/p&gt;
&lt;p&gt;At work, most of our models inherit from a &lt;code&gt;BaseModel&lt;/code&gt; class, which in turn inherits from a &lt;code&gt;ChangesMixin&lt;/code&gt; from the &lt;a href="https://github.com/iansprice/django-model-changes-py3"&gt;django-model-changes&lt;/a&gt; library. Adding the &lt;code&gt;ChangesMixin&lt;/code&gt; to our model is revealing:&lt;/p&gt;
&lt;div class="highlight not-prose text-sm rounded-lg overflow-x-auto" style="background: #1d2331"&gt;&lt;span class="filename"&gt;Text Only&lt;/span&gt;&lt;pre style="line-height: 125%;"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;In [1]: %timeit list(Dese.objects.all())
3.4 s ± 61.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We're now at 3.4s for 100,000 objects, a roughly 7x slowdown versus not using &lt;code&gt;ChangesMixin&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;To be continued...&lt;/p&gt;</content><category term="programming"></category></entry></feed>