<?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-07-20T00: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 16</title><link href="https://blog.tmk.name/2026/07/20/good-list-week-16/" rel="alternate"></link><published>2026-07-20T00:00:00-04:00</published><updated>2026-07-20T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-07-20:/2026/07/20/good-list-week-16/</id><summary type="html">&lt;h2&gt;West Essex Trail&lt;/h2&gt;
&lt;p&gt;I signed up for another 5k (and am leaning towards a 10k as well) in order to keep myself running - I am &lt;em&gt;very&lt;/em&gt; external validation oriented. Feeling a bit tired of Brookdale Park I explored other options in my area and recalled the West Essex Trail - an …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;West Essex Trail&lt;/h2&gt;
&lt;p&gt;I signed up for another 5k (and am leaning towards a 10k as well) in order to keep myself running - I am &lt;em&gt;very&lt;/em&gt; external validation oriented. Feeling a bit tired of Brookdale Park I explored other options in my area and recalled the West Essex Trail - an old railroad line that's been converted to a walking, running and biking trail. It's well maintained, flat, and soft dirt which makes it ideal for long runs. The scenery is lovely, especially on a warm summer day, as the trail slices through the backyards of suburban houses, small businesses, and even crosses a small highway over an old railroad bridge.&lt;/p&gt;
&lt;p&gt;The total length is about 3 miles, which means for a 10k I'd run a full loop one way and then back again. I like that this provides a physical gauge of training progress, as I slowly add distance to my runs.&lt;/p&gt;
&lt;h2&gt;Karaoke&lt;/h2&gt;
&lt;p&gt;I organized a karaoke outing with friends last week, which I was quite nervous about - I have long held insecurities over my voice, especially singing. Going with trusted friends gave me the psychological safety necessary to open up, and that I did, belting out Mr Brightside, Ice Ice Baby, and All the Small Things. It may be that I am reading a book on evolutionary biology/psychology (&lt;em&gt;Moral Evolutions&lt;/em&gt;), because I began to see the communal emotional outlet of group making music and singing together. Certainly seeing my friends (and myself) express themselves so openly and honestly was deeply nurturing for me.&lt;/p&gt;
&lt;p&gt;&lt;img alt="karaoke" src="https://blog.tmk.name/images/gl16/karaoke.jpg" /&gt;&lt;/p&gt;
&lt;h2&gt;Climbing&lt;/h2&gt;
&lt;p&gt;Rounding out the week's adventures I went to a local climbing gym with my girlfriend. I had climbed once before, so had some basic idea of what I was doing, but clearly I had forgotten just how darn challenging it is to do even basic climbs. We started with bouldering, floundering around v0s and v1s, until Maria succeeded in finishing a climb. Before, I was hesitant about climbing too high and falling, but after a switch in me flipped (Maria said she could see my eyes changed) and I immediately scrambled up the wall to finish the same climb. The competitive spirit within me runs deep, clearly.&lt;/p&gt;
&lt;p&gt;We tried our hands at several of the easy bouldering climbs, and then moved onto rock climbing. I managed to climb about a third up the wall before jumping down, which I practiced repeatedly (perfect opportunity to face/accept/float a strong fear!). Maria went way up the wall, which my competitiveness was uninterested in besting, overridden by the height.&lt;/p&gt;
&lt;p&gt;After seeing how much we both enjoyed the experience (for similar and different reasons), we are definitely going back. Climbing meets some real psychological and physical needs: healthy risk and excitement, team-work, and goal accomplishment.&lt;/p&gt;
&lt;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl16/climbing1.jpg" class="glightbox" data-gallery="gallery-6" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl16/climbing1.jpg" alt="Climbing at the gym"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl16/climbing2.jpg" class="glightbox" data-gallery="gallery-6" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl16/climbing2.jpg" alt="Climbing at the gym"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl16/climbing3.jpg" class="glightbox" data-gallery="gallery-6" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl16/climbing3.jpg" alt="Climbing at the gym"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2&gt;Good List&lt;/h2&gt;
&lt;p&gt;And that leads me to the Good List itself. I can't believe I've kept at it for 16 weeks, or 4 months. It's been motivating in a positive way, as I have found I both look forward to posting what I've been up to, as well as challenging myself to try new experiences to post about. Maybe influencer is a potential career change for me, haha.&lt;/p&gt;
&lt;p&gt;I have decided to take a break from the posts, to give myself some time and space to reflect on the process (maybe irregular one offs instead of strict weeklies), and to focus my time and attention on writing my new recovery series, which is currently near and dear to my heart.&lt;/p&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Recovery: Burnout - Origins</title><link href="https://blog.tmk.name/2026/07/14/recovery-burnout-origins/" rel="alternate"></link><published>2026-07-14T00:00:00-04:00</published><updated>2026-07-14T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-07-14:/2026/07/14/recovery-burnout-origins/</id><summary type="html">&lt;p&gt;I've been blocked writing about my experience with burnout for a full year now. I feel so much fear and shame: I'm a failure and I shouldn't talk about it. It's even been hard for me to open up in safe spaces, with close friends - the message inside my head …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've been blocked writing about my experience with burnout for a full year now. I feel so much fear and shame: I'm a failure and I shouldn't talk about it. It's even been hard for me to open up in safe spaces, with close friends - the message inside my head of "here he goes again, talking about his brain fog for the hundredth time, why doesn't he stop feeling so sorry for himself and just push through?" That burnout is dominated by inner symptoms and relatively invisible on the outside means I am limited in receiving external validation, which has kept me locked up inside myself, perpetuating the very conditions that fuel burnout. Blah. It's a vicious cycle.&lt;/p&gt;
&lt;p&gt;As I described in my &lt;a href="https://blog.tmk.name/2026/07/06/recovery-anxiety/"&gt;article on anxiety&lt;/a&gt;, I think of the anxiety condition as a system of stored stress and fear. Part of the healing journey is in releasing these pent up emotions, and a key method for me is expressing my experiences in writing. It's my intent to share openly and honestly about my experiences, first to help myself relieve the fear and shame, and second to help others who are stuck in these emotions, bewildered as to their condition, its causes and its healing.&lt;/p&gt;
&lt;h2&gt;One year ago&lt;/h2&gt;
&lt;p&gt;Last July I caught the flu. It was a terrible case of the flu, I was knocked off my ass for a good week. Symptoms persisted for another week, then another week, and I dragged myself to the doctor to get tested, presuming that I had caught COVID again and could I get on some of those antivirals? I tested negative for COVID, positive for the flu, and going back a couple weeks later once more tested positive for the flu a full month out. My physical prowess recovered, and I was able to move through life, but my brain kept the score: I experienced persistent brain fog. It's taken many aspects over the past year, it swings up and down and back and forth in intensity. Some of the ways I've described my brain fog are mental dullness, lack of sharpness, sluggishness, and like a match that just won't light.&lt;/p&gt;
&lt;p&gt;This "long flu" terrified and perplexed me. When will it fade so I can get back to my old self? Why do I have good days and bad days, good weeks and bad weeks? Most frustrating - doesn't my body understand that in order to correct my environment to reduce stress I need my full cognitive capacity? Where's the fairness in a condition that prevents me from addressing it?&lt;/p&gt;
&lt;p&gt;In hindsight, now, I can see the progression of the condition. I can see the alarms flashing. Back at the time, though, my world shrunk to getting through each day, hyperfixated on managing my various crises. I can't say what could've been if I had had someone to sit me down, slap me, and thoroughly explain my condition and how to act skillfully. Perhaps I would've dutifully nodded, and immediately returned to crisis management. It's a really hard mental habit to break.&lt;/p&gt;
&lt;h2&gt;Crisis management&lt;/h2&gt;
&lt;p&gt;In the month preceding the flu, I ran the stress gauntlet.&lt;/p&gt;
&lt;p&gt;I had started applying to new jobs, playing the market to see if any opportunities interested me. I was immensely fortunate, out of a dozen or so applications I found myself in several active interview cycles. This quickly proved to be too much for me. I have intense test anxiety, owing to growing up with perfectionistic expectations around grades. Interviews that take the form of proctored tests trigger this anxiety quite strongly. I noticed that I was overdoing it and withdrew my applications, but that stress had been seeded.&lt;/p&gt;
&lt;p&gt;I started dating a new woman. I was instantly obsessed. It's really incredible how my brain picks up on subtle cues that hearken back to childhood and past relationships, how I latch onto someone I don't know rationalizing the overwhelming feelings as "passion." This new relationship had all the hallmarks of unhealthy codependence: intense initial attraction, over-sharing, boundary violations, love-bombing. I recognized these patterns in real time but I struggled to escape; I was hooked. After a fight a month in I gathered my courage and ended the relationship. I was left reeling, deeply vulnerable after this experience.&lt;/p&gt;
&lt;p&gt;Most significant, one of my reports at work &lt;em&gt;went through his own breakdown&lt;/em&gt;. This tested me on so many levels: as a recovering codependent, I had to maintain strict boundaries with myself to not try to fix or caretake my employee. I also had to set boundaries with my own boss: as a brand new people manager with no training under my belt, I decided to turn over the situation to my boss and HR, and follow their instructions. Unbeknownst to me at the time, my boss has his own undiagnosed abandonment issues. While my employee spent multiple months contributing nothing or next to nothing, my boss purposefully misled, gaslit, and abused me over the situation.&lt;/p&gt;
&lt;h2&gt;Burning out at work&lt;/h2&gt;
&lt;p&gt;This latter situation was especially distressing because it mirrored what I experienced growing up. When my father and stepmother divorced, my father placed me in the care of my now ex-stepmother, despite full awareness that she was mentally unwell and could not provide a safe environment for a child. As the abuse mounted by my ex stepmother, my father turned the tables and told me that it was my fault, my choice, and that really &lt;em&gt;I was the one abandoning him&lt;/em&gt;. At ten years old, I had no power to challenge him, and I internalized this guilt. Over the years, my ex stepmother spiraled down into depression and drug addiction, and at every juncture my father repeated the same lies: that I was responsible for my ex stepmother, that he was the true victim.&lt;/p&gt;
&lt;p&gt;When my employee broke down, it took me back to when my ex stepmother broke down. I was at the prestigious governor's school summer program for gifted high schoolers. At a time when I should have been fully engrossed in the experience, making friends, learning, and playing, I was on the phone begging my father to help my ex stepmother whose drug addiction had escalated to the degree of erratic and dangerous behavior. At work last year, I had to beg my boss to help me with my employee, to provide minimal guidance and support. My boss not only refused to help, but actively misled and manipulated me, making me out to be responsible for my employee's breakdown and lack of recovery.&lt;/p&gt;
&lt;p&gt;I kept my head down and tried to manage the work situation for several months before I realized that the toll on me was too great, that there was no path to resolution: my boss said that firing is off the table, so short of a spontaneous and miraculous recovery, the status quo would persist endlessly. I withdrew from managing this employee, offering my full support to help with the transition but making it clear that I had to step back for my own sake.&lt;/p&gt;
&lt;p&gt;The other half of narcissistic abuse comes after you set a boundary and step back. I had naively hoped that by playing it nice, not naming the abuse, I could placate my boss. Just as when I was a child dealing with narcissistic parents, it didn't matter how small I made myself, how little I spoke up for myself, how much abuse I accepted by turning the other cheek. After stepping back from managing this employee in crisis, my boss began a targeted campaign to force me out. Every week there'd be some new abuse. I thought I was handling it well by continuing to not engage, not react, keeping my profile as small as possible.&lt;/p&gt;
&lt;p&gt;When a teammate suddenly departed, I saw how my boss was crushed, actually devastated. He honestly believed he could convince this person to stay, when they had already accepted an offer elsewhere. I had a lot of empathy for my boss at that moment, seeing how his narcissism was sourced from the same abandonment wounds that scarred me. My boss for his part felt shocked and laid off the abuse for a couple weeks - but the narcissistic drive to dominate or exile me quickly returned. The "fix was in" when I got my performance review that made no mention of the months I spent managing my employee in crisis. A week later my boss had a meltdown and threw a temper tantrum alternating between begging me to stay and threatening to fire me.&lt;/p&gt;
&lt;p&gt;I had been holding on for months, rationalizing it as accumulating savings for a period of time off to rest and recover. After the gaslighting performance review, the part of me that was holding on let go. After the temper tantrum, I knew it was time to go. I walked away from a cool ten to fifteen grand by quitting before the end of the year. &lt;/p&gt;
&lt;h2&gt;Warning signs&lt;/h2&gt;
&lt;p&gt;From June to December, I spent a solid six months in acute and intense stress and anxiety. Looking back I don't know how I managed to make it work for so long. I suppose it's because it felt familiar, that I was trained for this in my childhood. While my mind was reeling from the stress and brain fog, my body was marking the score. Long flu was the first physical ailment, rare and usually responded to with "don't you mean long COVID?" Next was jaw tension - my mouth and jaw were practically locked tight, with night time grinding, leading to aches and pains during the day. When I went to the dentist, they also found a reinfection of a tooth that had previously been treated with a root canal - a root canal that the endodontist said was perfectly done. I was told that reinfection is exceedingly rare. But I wasn't surprised. I knew that months of stress was causing my body to deteriorate, to be prone to infection and reinfection and reducing my body's ability to fight off infection. Finally my gastric system started acting up, with bowel distress daily.&lt;/p&gt;
&lt;p&gt;More than the cognitive symptoms, the physical symptoms really spooked me into leaving the job and taking time for recovery. As uncomfortable as an emergency root canal is, it was the wake up call I needed that I had to make a change to right my trajectory. My body was screaming out to me.&lt;/p&gt;
&lt;p&gt;It took me a long time to listen, but I'm thankful I did because not everyone can stop the process of self sabotage before their life is in shambles, or worse. I left my job with my head held high and finally started on the long journey of recovery.&lt;/p&gt;</content><category term="recovery"></category></entry><entry><title>Good List Week 15</title><link href="https://blog.tmk.name/2026/07/13/good-list-week-15/" rel="alternate"></link><published>2026-07-13T00:00:00-04:00</published><updated>2026-07-13T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-07-13:/2026/07/13/good-list-week-15/</id><summary type="html">&lt;h2&gt;Anderson Park&lt;/h2&gt;
&lt;p&gt;One of my favorite ways to spend a spare hour is to bring a book and coffee to Anderson Park in Montclair. The park itself is lovely, a sprawling bowl of green grass where frisbee, volleyball and soccer is played by athletic youths. Around the central grass the …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Anderson Park&lt;/h2&gt;
&lt;p&gt;One of my favorite ways to spend a spare hour is to bring a book and coffee to Anderson Park in Montclair. The park itself is lovely, a sprawling bowl of green grass where frisbee, volleyball and soccer is played by athletic youths. Around the central grass the walking path has several shaded benches, ideal for plopping down and nestling into a book (last week's read was &lt;em&gt;A Brief History of the Earth&lt;/em&gt; by Andrew Knoll, a fascinating tour of the planet's and our own evolution in time and space). Sometimes I'll bring my picnic blanket and spread out on the grass, lying and taking in the warm summer air.&lt;/p&gt;
&lt;h2&gt;Nature Walk&lt;/h2&gt;
&lt;p&gt;For my artist's date this week (it's hard to believe I'm already on week 11! time flies!), I went to Eagle Rock Reservation for a nature walk. I had purchased a loupe (which happened to be a recent crossword answer, serendipitous!), for peering into the depths of moss and leaves and such. Turns out that with a loupe the target must be basically pressed against my face, which was beyond my comfort level to do against the ground or a tree. Loupes aside, I brought a notebook refashioned as a field notebook and pretended to be a wildlife biologist (inspired by a recent read of &lt;em&gt;Annihilation&lt;/em&gt;), recording observations on plants and animals. As luck would have it, I got a great look at a fox who I surprised in the woods, a couple deer, and even a large frog swimming in a small puddle left over from recent rains. My inner artist was very pleased.&lt;/p&gt;
&lt;h2&gt;The Invite&lt;/h2&gt;
&lt;p&gt;On Friday I had been feeling some kind of way, so I decided to cancel plans and take myself to the movies. I saw &lt;em&gt;The Invite&lt;/em&gt;, on the basis of some youtube trailer reviews, and was pleasantly surprised to find myself laughing throughout the movie. The plot revolves around a dinner invite from a couple to their upstairs neighbors. From the get-go, the dialogue and acting were superb, I especially enjoyed the dynamic between Seth Rogen and Olivia Wilde's characters.&lt;/p&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Katamari</title><link href="https://blog.tmk.name/2026/07/10/katamari/" rel="alternate"></link><published>2026-07-10T00:00:00-04:00</published><updated>2026-07-10T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-07-10:/2026/07/10/katamari/</id><summary type="html">&lt;p&gt;I've been playing my nintendo switch again, grabbing games from the library. First, what a great deal, it reminds me of going to blockbuster as a kid, except there's no fee to rent, and generous return turnarounds. Second, it's been fun to play random indie games alongside more popular hits …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've been playing my nintendo switch again, grabbing games from the library. First, what a great deal, it reminds me of going to blockbuster as a kid, except there's no fee to rent, and generous return turnarounds. Second, it's been fun to play random indie games alongside more popular hits. I grabbed Once Upon a Katamari a couple weeks back, and what a blast, it took me right back to playing Katamari on my playstation 2 as a teenager.&lt;/p&gt;
&lt;p&gt;For the uninitiated, Katamari is a game with a sacred mission: rolling up the world into a giant ball. You start small, maybe a few centimeters in size, rolling up paper clips and peanuts. Then, as you grow, you start rolling sushi rolls and playing cards, then small animals, then big animals, then people, bushes, trees, houses, and eventually islands. It's as silly as it sounds.&lt;/p&gt;
&lt;p&gt;Reading the Artist's Way, and getting into a daily schedule, I wanted to get back into what it feels like to &lt;em&gt;play at work&lt;/em&gt;. So I started where I started as a teen: making games in the terminal. Back then, it was battleship printed to the console; that evolved into curses games, followed by graphical games with sdl2 (C) and libgdx (java) and pygame (python).&lt;/p&gt;
&lt;p&gt;&lt;img alt="screenshot" src="https://blog.tmk.name/images/battle.png" /&gt;&lt;/p&gt;
&lt;p&gt;I made a simple Katamari-inspired game in the terminal this week (code-named &lt;em&gt;Battle&lt;/em&gt;, you can see my creative energy went into the game not the name!) - check it out &lt;a href="https://github.com/tristanmkernan/battle"&gt;here&lt;/a&gt;. The object is the same: roll over other players of lower levels to accumulate them and grow in size, while avoiding higher level and larger players.&lt;/p&gt;
&lt;p&gt;It was fun, even doing 2d math in the convoluted dimensions of curses where (0,0) is the top left corner, and positive is down and right. I set a daily goal of 1 hr and regularly exceeded this goal to the point I had to hold myself back (pacing works in both directions, doing not too much and not too little). I am pleased with how it came out, though not perfectly pleased - it has some rough edges and bugs remaining, but I'm intentionally practicing marking it done now so as to also get in the habit of shipping imperfect code over endlessly tinkering (how many half-done projects lie in my github private-repo graveyard!).&lt;/p&gt;
&lt;p&gt;As Julia Cameron said, when work is fun, it's a lot easier than when work is forced and perfectionistic. Thank you, Julia!&lt;/p&gt;</content><category term="programming"></category></entry><entry><title>Recovery: Anxiety</title><link href="https://blog.tmk.name/2026/07/06/recovery-anxiety/" rel="alternate"></link><published>2026-07-06T00:00:00-04:00</published><updated>2026-07-06T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-07-06:/2026/07/06/recovery-anxiety/</id><summary type="html">&lt;p&gt;I have been thinking a lot about anxiety recently. I recently read the wonderful Claire Weekes' book &lt;em&gt;Hope and Help for Your Nerves&lt;/em&gt;. Ironically I was anxiously blocked to read the book; I had it on my shelf for almost two full months, I picked it up and started it …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have been thinking a lot about anxiety recently. I recently read the wonderful Claire Weekes' book &lt;em&gt;Hope and Help for Your Nerves&lt;/em&gt;. Ironically I was anxiously blocked to read the book; I had it on my shelf for almost two full months, I picked it up and started it two times before putting it back down again. Third time's the charm, I read it cover to cover.&lt;/p&gt;
&lt;p&gt;How could I be anxious about reading a book on anxiety? I believe that anxiety, as a coping mechanism, is strongly defended and rationalized by the mind: at a certain point, I'd prefer to remain anxious and blocked than face my fears and move forward. Claire Weekes, with simple and penetrating insight, fully reveals anxiety as the emperor without clothes; my psychological defenses ramped up to prevent this realization from bubbling up, sending the very fear response I'm seeking to face to overwhelm me and cause an avoidance behavior.&lt;/p&gt;
&lt;h2&gt;Anxiety as bewilderment&lt;/h2&gt;
&lt;p&gt;Claire Weekes does a great job at listing the various ailments that anxiety sufferers face. I made several new connections myself; while I had been aware of the more obvious symptoms like body tension, shortness of breath, and susceptibility to illness, I saw how episodes of low blood sugar leads to weakness and panic, my bowels are thoroughly permeated with anxiety, and most poignantly that avoidance is an anxiety response. The anxiety sufferer begins with bewilderment of such symptoms, worrying if they've become the victim of some terrible disease like cancer. Health anxiety and internet doomscrolling has been my bread and butter for ages; ordering random supplements and convincing myself I must have some terrible condition lurking within me.&lt;/p&gt;
&lt;p&gt;That so many of my health symptoms can be plausibly traced to anxiety seems obvious in hindsight. &lt;em&gt;The Myth of Normal&lt;/em&gt; by Gabor Mate is relevant here, with its thorough accounting of the various ailments affecting trauma and anxiety sufferers. The story that sticks with me is of the doctor's office that noted its patients tended to be the most pleasant people, in other words people pleasers. The behavior tendency to suppress one's emotions, especially the most challenging like guilt, shame, fear, and pain, is directly linked to rare disorders of the body. As a wake up call, I took home the critical importance of continuing on my healing journey and seeking out safe loving communities.&lt;/p&gt;
&lt;h2&gt;Anxiety as avoidance&lt;/h2&gt;
&lt;p&gt;Avoidance is maybe the most subtle symptom of anxiety for me. Through rationalization and denial I am typically not even aware I am avoiding some task, person or feeling of which I am afraid. One clear example is addressing health issues promptly. I've had my share of bodily injuries, but what sets me apart is waiting weeks, months, or even years to seek treatment, all the while convincing myself that I am definitely taking care of myself! Once I do seek treatment, any bump in the road sets me back on the path of avoidance. When I injured my foot, a podiatrist told me that they had no idea what was wrong and good luck. Instead of seeking a second opinion, I took this to heart and continued suffering in silence for many more months before taking another positive step forward. I now make a conscientious effort to address health issues promptly, and encourage others to do the same. So much suffering is avoidable just by taking the first few positive actions.&lt;/p&gt;
&lt;p&gt;Another major area for avoidance for me is relationships. Growing up in a dysfunctional family, I learned that speaking up for myself was dangerous and liable to be responded to with shame, guilt, neglect and abandonment. As an adult, I've swallowed my feelings, my wants and needs, my interests, my complaints, out of fear. But it was so second nature that I wasn't even aware of it, I rationalized my behavior as normal, if not outright heroism: here &lt;em&gt;I&lt;/em&gt; am taking the high road by putting someone else first! &lt;em&gt;I'm&lt;/em&gt; not going to turn out like my parents, explosive and selfish and rageful. Lo and behold, as the ancient Greeks said, the very steps I took to avoid my fate led me directly to it. I was thoroughly dysfunctional myself, just in the inverted, codependent-dominant way.&lt;/p&gt;
&lt;p&gt;It took many painful friendships, relationships, and jobs to see my role in my unhappiness. It took recovery for codependence for me to see how I was sabotaging myself by not speaking up for myself, setting boundaries, and ending unhealthy relationships promptly rather than sticking around and trying to fix them. It took years after starting to put new behaviors in practice for me to begin to see how deep-rooted the fear is within me, how I've been running from that ancient, primordial fear of abandonment my entire life.&lt;/p&gt;
&lt;p&gt;Other avoidance strategies that I employ are not getting mail from my mailbox for weeks (and then leaving the mail unopened on my table); using "safe" chores like dishes, laundry, cleaning to fill in empty time; running errands to businesses unnecessarily far away to add extra driving time; picking up my phone or alt tabbing to the browser to doomscroll social media and the news. Like I said, it's pervasive in my life.&lt;/p&gt;
&lt;p&gt;I rank avoidance as one of the harder symptoms of anxiety to resolve, because the mind is so capable of hiding it. I'm putting into practice new mindful approaches to how I spend my time, in order to seek out, explore and understand my avoidant behaviors. One of my favorite avoidant strategies is constant busy-ness, so I'm optimistic that casting a compassionate eye on my time management will reveal underlying fears and let me face them with love and compassion.&lt;/p&gt;
&lt;h2&gt;Anxiety in the body&lt;/h2&gt;
&lt;p&gt;When I started anxiety recovery, I was completely numb to my body. I was aware of intense sensations, like sharp pain or orgasm, but numb to the every day average sensations that comprise the vast bulk. What helped me to start to get in touch with my body was emotional recovery through program, consistent meditation practice, and finally yoga. The timeline was incredibly slow, more like evolutionary punctuated equilibrium than steady progressive gains over time. The fits and starts have been meaningful, and revelatory over time, but I'm far from fully in tune with myself, I only just became aware of my consistent chest tension (thanks Claire Weekes!).&lt;/p&gt;
&lt;p&gt;The first bodily sensations I noticed were tendonitis, as they were chronic and loud. I developed some tendonitis in my forearm (tennis elbow) after over-doing it at the gym, but I rank the real underlying cause to be my unhealthy relationship to work. Showing up at the computer with my hands and forearms taut with stress for hours every day for work, followed by extra hours of personal use, with breaks to use my phone.. yeah, repetitive strain is all but guaranteed.&lt;/p&gt;
&lt;p&gt;Next was my back and shoulders. I began to notice my back when I started going for massages - the masseuses exclaimed shock at how knotted my back was. I was aware without their surprise, as their hands punished me and I called it quits when it was too much (quick comparison to today when a back massage is deeply relaxing and soothing). Yoga has helped me tremendously at unlocking the tension stored in my back and shoulders, and helping to keep that stress moving in and through me rather than accumulating.&lt;/p&gt;
&lt;p&gt;Next was my breathing. This has come in fits and starts, at first I became aware during some meditations that I'd go through cycles of not breathing and then gasping for air. Learning and practicing paced breathing methods like resonance breathing (5 seconds in no hold 5 seconds out no hold repeat), 4-7-8 (4 seconds in hold 7 seconds 8 seconds out no hold repeat) and box breathing (equal 4 seconds each in, hold, out, hold) has helped me tremendously in this area. A breakthrough for me was that a good meditation does not require sitting with intense anxiety for long stretches; instead, I learned that calming is part of the practice, and sitting with guided resonance breathing for twenty to thirty minutes is superior to the same duration gasping for air.&lt;/p&gt;
&lt;p&gt;One marker of my growth in this area came during my recent 5k training. I started training when on a no-tech week, so I ran without phone or music. I enjoyed the time with myself and my body enough that I kept at it through the entire training and race. It was great practice to check in with myself, to observe how my calves felt at the start, middle, and end of a race; to be fully present with my breathing; to notice how my body started to unwind itself and stretch out. Writing this now I am blushing as I recognize my perfectionism showing up; this &lt;em&gt;is&lt;/em&gt; what it's like to be present with my body, to be aware of myself.&lt;/p&gt;
&lt;h2&gt;Anxiety as a positive feedback loop&lt;/h2&gt;
&lt;p&gt;The last topic I want to share on today is the stressor-fear-stressor cycle of anxiety and panic: &lt;/p&gt;
&lt;div class="mermaid"&gt;graph LR
    A[Stressor] --&amp;gt; B[Fear]
    B --&amp;gt; A&lt;/div&gt;
&lt;p&gt;otherwise known as the first and second fears, or the two arrows in Buddhism. I find it meaningful that many different cultures came up with the same understanding of how fear works.&lt;/p&gt;
&lt;p&gt;The basic process is as follows: some stressor triggers an automatic fear response within me, which causes some set of body and mind sensations, which generates more fear, cycling adding more fear until I'm in a state of panic and collapse. The wise or skillful approach is to catch myself, at any point in this cycle, and respond without fear. In the best case, this leads the cycle to fizzle out, as without more fear added the stress will naturally fade away (how hard it is to remember this in the moment!). Typically, the fear lingers or even continues to grow, but this does not reflect poorly on me, as the panic cycle is well worn and deeply grooved within me; it takes significant repetition and practice to train the mind to respond differently.&lt;/p&gt;
&lt;div class="mermaid"&gt;graph LR
    A[Stressor] --&amp;gt; B[Fear]
    B --&amp;gt; C[Acceptance]
    C --&amp;gt; D[Resolution]&lt;/div&gt;
&lt;p&gt;This is an example of a &lt;em&gt;positive feedback loop&lt;/em&gt;, which despite the name is not so positive when experienced as a panic attack. The classic example of this process is two telephones on call together held next to each other, where the sound from one is amplified by the other, then received back and amplified further, cycling and amplifying more and more. It may also be thought of as an interest bearing account, where the interest is reinvested as capital, leading to more interest, leading to more capital, and so on.&lt;/p&gt;
&lt;h2&gt;Two pillars of recovery&lt;/h2&gt;
&lt;p&gt;Understanding the feedback loop dynamic completes the picture of my systems thinking of anxiety recovery, which I define with two main features. The first is the stored anxiety, pain, grief, trauma, etc., that inflames reactions towards the extreme. Emotional recovery, in the form of 12 step, trauma informed therapy, etc., thaws and processes these stored emotions to reduce the kindling for future emotional eruptions. The second feature is reducing the amount of new emotions bottled up. This means regular emotional processing of current events through journaling, intimate conversations, therapy, somatic work, etc. It also means interjecting new behaviors into the fear cycle by reacting differently in the moment, for example through Claire Weekes' face/accept/float/let time pass method.&lt;/p&gt;
&lt;p&gt;Put together, this leads to a state of baseline calm and less reactivity. In other words, nerves that have adapted to be calm and return to calm more readily. It takes enormous effort to do this work and make these changes, but I can't see any other way to live at this point.&lt;/p&gt;</content><category term="recovery"></category></entry><entry><title>Good List Week 14</title><link href="https://blog.tmk.name/2026/07/05/good-list-week-14/" rel="alternate"></link><published>2026-07-05T00:00:00-04:00</published><updated>2026-07-05T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-07-05:/2026/07/05/good-list-week-14/</id><summary type="html">&lt;h2&gt;New York Historical&lt;/h2&gt;
&lt;p&gt;For my artist date this week, I decided to go to a museum. I first went to the library to pick up a museum pass; from their available selection, I chose the New York Historical. I had never been, and it was a good excuse to get …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;New York Historical&lt;/h2&gt;
&lt;p&gt;For my artist date this week, I decided to go to a museum. I first went to the library to pick up a museum pass; from their available selection, I chose the New York Historical. I had never been, and it was a good excuse to get into the city, plus I knew that the museum has the epic &lt;em&gt;Course of Empire&lt;/em&gt; series by Thomas Cole.&lt;/p&gt;
&lt;p&gt;I had an uneventful time getting into the city, even with the heat wave. I met my sister for coffee. Then I ventured off to the museum, located just beside the Natural History Museum. I figured that the Historical would be small, judging from the outside, but it felt large once inside: three floors of exhibits, plus a mini movie theater with repeating documentaries on the hour. I noticed they host free tours, and I was just a little early so I explored on my own for a bit, chancing upon the new democracy exhibit which includes &lt;em&gt;Course of Empire&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Okay maybe I am a little obsessed with this series. I also do admit to having prints of &lt;em&gt;Voyage of Life&lt;/em&gt; hanging on my walls. What can I say, I love epics, and anything that gets to the cyclical nature of existence. Face to face with the series at last, I was impressed by the scale. It's one feature that computer screens can't replicate: the big-ness of the paintings, the relative scales between them (Consummation being larger than the other four). The level of detail is just incredible, with small stories told in the grander scheme: in the Arcadian, a solitary mathematician draws in the dirt, while a shepherd tends his sheep.&lt;/p&gt;
&lt;p&gt;I got onto the tour, which even with more than an hour skipped over multiple exhibits, focusing on some of the key items, mostly of primary historical and local interest: Washington's inauguration chair, Washington's army cot, Thomas Nast's snake vase. I was impressed to hear of the latter that Thomas Nast was offered a half million dollar bribe by Boss Tweed to quit making cartoons of him; Nast turned the money down and saw Tweed locked up in jail. That kind of moral virtue towards corruption in politics and business is sorely lacking today. By chance I just read Sarah Wynn Williams' &lt;em&gt;Careless People&lt;/em&gt;, and I am reminded of her own courage in sharing about Facebook's evil-doings at significant financial risk to herself (borne out in the punishments since doled out!).&lt;/p&gt;
&lt;p&gt;My inner teen is excited by stories of justice and courage like these. It's long been an inner drive of mine to call out injustice, and it has been a frequent source of conflict for myself at work, albeit the retaliation on a significantly smaller scale than Nast or Williams experienced. I'm also reminded of the line from &lt;em&gt;The Republic&lt;/em&gt;: justice is determined by those with the power to enforce it. Stories of courageous truth tellers remind me that the powerful are not invulnerable.&lt;/p&gt;
&lt;p&gt;&lt;img alt="course of empire" src="https://blog.tmk.name/images/courseofempire.jpg" /&gt;&lt;/p&gt;
&lt;h2&gt;5k&lt;/h2&gt;
&lt;p&gt;On July 4&lt;sup&gt;th&lt;/sup&gt;, I woke up at 5am to run a 5k race! It's not my first 5k, I ran one in college a decade ago. But I have not run since. I injured my foot some years back and stayed away from high-impact cardio. Recently, I've been feeling much better physically, and with a year of yoga under my belt, I felt ready to take on a new physical challenge. I trained on a modified couch-to-5k schedule, slowly ramping up mileage over the course of five weeks. I ran in rain, cold, heat, and wind; some days were quiet and peaceful, other days the wood chippers blared in my ears as if I was running from some mechanical nightmare.&lt;/p&gt;
&lt;p&gt;All in all I kept at it with my training schedule, and with the generous time allotted I ran 3 practice 5ks before the race. Talking with a friend, I set myself the final goal of finishing, healthy. My secondary goal was a 45 minute finish; my training times were in the 46-48 minute range so this felt do-able.&lt;/p&gt;
&lt;p&gt;Day of the race, my girlfriend asked me how I was feeling, and I was surprised to find myself saying that I felt totally indifferent. How could that be?! I had spent a month training 3x per week. I realized this was a fear response; avoidance. Growing up, I was expected to be perfect. This started with school grades: report cards were practically judgments from God, as anything less than straight A's meant intense shaming and guilting ("Mr A Minus" was my father's nickname for me after one imperfect report card). Beyond school, I ran track for a semester in high school, where I was mostly motivated by chasing girls and copying my older brother. My father showed up for one track meet before telling me, "Why would I come to watch you lose?" and never showing up again (including the regional track meet where, in the middle of the race, my shoes came off and I finished in just my socks - my coach was quite proud that I didn't give up).&lt;/p&gt;
&lt;p&gt;Needless to say, I had my share of baggage around running, so shutting down before the race was a defense mechanism to avoid past pains. I comforted my inner teenager who felt so hurt, and I embraced the race as best I could, knowing that like most teens, he'll come around eventually. That's how I found myself Saturday morning, eating my hardboiled eggs and heading to the race. The weather reports were iffy ahead of time; that morning it was a nice and dry 80 degrees at 8am. I was joined on the race by a friend, and we paced each other the entire time. I felt great running, the energy from the other runners and supporters was infectious. It showed in my performance: I averaged 12 minute miles and finished in 37 minutes flat! I was shocked, I beat my wildest expectations.&lt;/p&gt;
&lt;p&gt;My automatically-moving-goalposts ramped up that evening, and I found myself looking up 10k races and training schedules. After some calm, I decided to do some exploratory training towards a 10k, with the goal of running a Halloween race (there's a race in Sleepy Hollow - how spooky!). If I like the training, and find it fits well into my structure, then I'll keep at it. If not, I'm thrilled that I set and achieved this personal goal.&lt;/p&gt;
&lt;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl14/midrun.jpg" class="glightbox" data-gallery="gallery-7" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl14/midrun.jpg" alt="Mid run"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl14/finish.jpg" class="glightbox" data-gallery="gallery-7" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl14/finish.jpg" alt="Finish line"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl14/poster.jpg" class="glightbox" data-gallery="gallery-7" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl14/poster.jpg" alt="Poster"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Recovery: Codependence</title><link href="https://blog.tmk.name/2026/06/30/recovery-codependence/" rel="alternate"></link><published>2026-06-30T00:00:00-04:00</published><updated>2026-06-30T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-06-30:/2026/06/30/recovery-codependence/</id><summary type="html">&lt;p&gt;I wasn't sure what to write about this week.. meditation? supplements? attachment? But then I was reminded, the best topic to share on at any given time is the one dearest to my heart at that moment. Today that is most definitely codependence.&lt;/p&gt;
&lt;p&gt;What is codependence? Many people have their …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I wasn't sure what to write about this week.. meditation? supplements? attachment? But then I was reminded, the best topic to share on at any given time is the one dearest to my heart at that moment. Today that is most definitely codependence.&lt;/p&gt;
&lt;p&gt;What is codependence? Many people have their own definitions, mine is inspired by Melody Beattie, my work in 12 step, and my own experiences. The main component is &lt;em&gt;dependence&lt;/em&gt;: when I am no longer able to operate independently of another person. In the past this has ranged from complete obsession to more subtle, covert addiction. Within this structure, typical patterns play out: the drama triangle from victim to victimizer, the date-breakup-date cycle, explosive highs and destructive lows.&lt;/p&gt;
&lt;p&gt;&lt;img alt="drama triangle" src="https://blog.tmk.name/images/dramatriangle.png" /&gt;&lt;/p&gt;
&lt;p&gt;I was made aware of my codependent behaviors when I read &lt;em&gt;Codependent No More&lt;/em&gt; by Melody Beattie. This book was a mirror into my spirit, it was as if Melody knew me better than I knew myself. Most painful and challenging for me was the realization that I was not a victim in my past relationships, as I had believed. After all, I wasn't the one exploding, picking fights, initiating breakups, threatening, testing, etc., I was the victim of such behaviors by my exes. While I wasn't engaged in such overt behavior, I was consumed by covert behavior: passive aggressiveness, withdrawal, withholding affection, conflict avoidance, lack of boundaries, lack of authenticity, etc.&lt;/p&gt;
&lt;p&gt;Seeing my part in the drama was painful. Even more painful was the realization that my behavior was not disinterested: I got payoff by staying in codependent relationships. By dating emotionally unavailable women, I could focus totally on chasing and fixing and rescuing, and remain completely numb to my own needs and emotions. In recovery, I've learned to break this pattern, and focus on myself, which has been an incredibly painful and challenging procedure as I have decades of grief and shame to process. That was my payoff: keeping my pain exiled, out of sight and out of mind.&lt;/p&gt;
&lt;p&gt;The other payoff I got was the explosive highs. Being so numb to myself, so disconnected and dissociated, it took extreme activation to pierce through and impact me. The whiplash from breaking up the day before to expressing mad love the next gave me the high I desperately craved, the high strong enough to temporarily abate the pain and overwhelm me with affection.&lt;/p&gt;
&lt;p&gt;Thankfully it's no mystery where this behavior stems from. As Claire Weekes' puts it, I was trained for it. My mother, an alcoholic, was obsessed with me as a small child. When she lost custody of me, I experienced profound abandonment: overnight I lost my mother, brother and sister. I was then raised by my stepmother, also obsessed with me, but prone to violent and random outbursts of rage. My father, for his part, abandoned me to the care of my stepmother, while himself ranging emotionally from mockery to rage to icy indifference.&lt;/p&gt;
&lt;p&gt;The template for unhealthy attachment style was programmed into me. Anxious-avoidant, craving the safe and loving affection I never received, while terrified of it; like Tantalus, doomed to reach for something always just out of range. So I spent my teenage and adult years going from one codependent relationship to another. If my partner wasn't buying it, if my partner brought a healthier attachment style to the table, I ran away.&lt;/p&gt;
&lt;p&gt;Now, with the help of recovery, I feel I am in a much healthier attachment style, though far, far, far from perfect. Just recently I've been feeling the codependent urges: if my girlfriend won't pick a fight, then dammit I'll pick one! I still crave the highs and lows, the overwhelming excitement and terrifying abandonment. When the urges arise, I have tools available to me now: call a fellow, go to a meeting, journal, check in with myself. Usually it's a sign that I'm overextending myself for others, and not taking care of myself.&lt;/p&gt;
&lt;p&gt;For instance, I've been helping my girlfriend with adulting recently, and I noticed a feeling of resentment creeping up. Where's my payday? When do I get to be the center of attention? Who's taking care of me? As hard as it is to share these thoughts and feelings, it's critical for me to do so, because it's when I keep them locked up inside that they build and build and then explode. The thoughts are also intimately revealing: I am feeling like I'm doing too much for others.&lt;/p&gt;
&lt;p&gt;Today I can validate that feeling for myself, and recognize that no one has been forcing me to help them, I've been taking this action of my own accord, and I can re-evaluate my involvement with others at any time. Right now, that looks like re-orienting toward myself: focusing on my wants and needs this week, and when someone asks for help (or I imagine they're asking for help, how often I confuse the two), I can turn them over to God and trust that they can take care of themself, just as I am doing for myself.&lt;/p&gt;</content><category term="recovery"></category></entry><entry><title>Good List Week 13</title><link href="https://blog.tmk.name/2026/06/29/good-list-week-13/" rel="alternate"></link><published>2026-06-29T00:00:00-04:00</published><updated>2026-06-29T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-06-29:/2026/06/29/good-list-week-13/</id><summary type="html">&lt;p&gt;It's the end of June; summer is in full swing (I've got to get to the beach still!). There's a heatwave this week, I'm hunkering down with the A/C on, hoping that there's a break in the heat for my 5k run this coming weekend. The past week was …&lt;/p&gt;</summary><content type="html">&lt;p&gt;It's the end of June; summer is in full swing (I've got to get to the beach still!). There's a heatwave this week, I'm hunkering down with the A/C on, hoping that there's a break in the heat for my 5k run this coming weekend. The past week was slower, but felt faster - I notice that my inner energizer bunny is ramping up. I'm planning to be intentional this week about slowing down in volume and in individual efforts.&lt;/p&gt;
&lt;p&gt;What about last week?&lt;/p&gt;
&lt;h2&gt;Obsession&lt;/h2&gt;
&lt;p&gt;I had a premonition that I'd relate to this movie, and I sure did. I thankfully went with some friends, and was blessed with a good crowd in the theater: there were as many laughs as gasps and moans. To say that I relate to Bear, well, it's painful to admit to myself and the world but it's oh so true. My relationship with my stepmother growing up, as well as my romantic relationships, closely modeled Bear and Nikki: I was unable, unwilling, or did not want to break off unhealthy relationships. When I hear others criticize Bear as the bad guy in the movie, well, they're not wrong. It was selfish of me to stay in unhealthy relationships, and I played a major part in the dysfunction.&lt;/p&gt;
&lt;p&gt;The movie itself was fantastic, it seems that we're in an era of quality low budget horror movies. That hints to me that as a society we're seeking our own terror and pain reflected back to us on the screen.&lt;/p&gt;
&lt;h2&gt;Cold Plunge&lt;/h2&gt;
&lt;p&gt;I had heard about cold plunges for years; I had my first exposure to Wim Hof some years ago. I dutifully tried cold showers, but once winter rolled around I couldn't stand the intense chill. I've recently been getting back into the breathing exercise and cold showers after reading &lt;em&gt;Breath&lt;/em&gt;. I wanted to fulfill the full journey with a true cold plunge, though, so I recruited my girlfriend and we booked a session at a contrast studio. It was way too expensive, given that what's provided is a hot tub and a cold tub, but options were limited and we were excited to do it so we figured let's splurge.&lt;/p&gt;
&lt;p&gt;The studio is located inside an upscale gym, so we got to shower and hit the sauna before dipping in. As my focus was on anxiety recovery through activation and relaxation, we spent the majority of our time in the hot tub, soaking in the warmth. The basic structure was a few minutes in the hot tub, followed by a brief cold dip, and repeat. The tub was 50 degrees fahrenheit, which I way underestimated as "not that cold" - my bones were aching within a few seconds in the water. I faced my fear of the cold and submerged myself over and over, noticing that I became anticipatory towards it, my heart pounding. I managed to stay in the cold about ten to twenty seconds at a time, gradually extending the time, and ultimately submerging my head helped.&lt;/p&gt;
&lt;p&gt;I noticed pretty clearly my thought patterns and automatic/body reactions. As I said, my heart was pounding in anticipation of entering the cold. Once in the water, the fear signals were overwhelming, which led to a panicked mind. While I was exercising my nervous system, I became obsessed with elongating each cold dip: I had to stay in longer than my girlfriend, certainly, and I should really be staying in the recommended full three minutes! I wasn't able to override this thought pattern, but I was able to be aware of it and not judge myself, as perfectionism is how I was raised and continues to haunt me.&lt;/p&gt;
&lt;h2&gt;Empty Cloud&lt;/h2&gt;
&lt;p&gt;I have been attending meditation sessions at &lt;a href="https://emptycloud.org/"&gt;Empty Cloud&lt;/a&gt; for over three years. I feel so grateful to have access to an incredible space so close to me in West Orange. The grounds are an old Christian monastery, evident in the architecture, crosses and old wall paintings. Empty Cloud is run by monks who live there full time (though they travel quite a lot). The monastery offers retreats for laypeople, and weekly programs. I usually attend on Saturdays for the mindful evening program, typically structured as 30 minutes of meditation, 30 minutes of dharma talk/lecture, and 30 minutes of q&amp;amp;a with the monks. Each monk who leads the program has their own twist, and it's been my pleasure to learn from so many different monks over the years.&lt;/p&gt;
&lt;p&gt;In particular I've enjoyed the different meditation practices: body scan, loving kindness, elements, emptiness, instruction-less. I always get something out of the talks, and my favorites are always on the basics, which I feel are practical for my every day life: witnessing, making small changes, accepting, letting go.&lt;/p&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Recovery: Meetings</title><link href="https://blog.tmk.name/2026/06/23/recovery-meetings/" rel="alternate"></link><published>2026-06-23T00:00:00-04:00</published><updated>2026-06-23T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-06-23:/2026/06/23/recovery-meetings/</id><summary type="html">&lt;p&gt;This is the first post in a new weekly series on recovery. Growing up in a dysfunctional family, I was ill prepared to meet life on life's terms. I've experienced depression and anxiety most of my life. I have blown up my life multiple times, dropped out of college, lost …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is the first post in a new weekly series on recovery. Growing up in a dysfunctional family, I was ill prepared to meet life on life's terms. I've experienced depression and anxiety most of my life. I have blown up my life multiple times, dropped out of college, lost all my friends, abused drugs, burned out, struggled with my health, went no contact with my parents, quit jobs. I've been addicted to video games, social media, pornography, work, drugs, dating.&lt;/p&gt;
&lt;p&gt;I've been through a lot. Putting it mildly. Speaking at rehabs for the past year has shown me that I have a story to tell, that my experience, strength and hope can be of service to others who are struggling, many who struggle without knowing what it is that ails them. I spent most of my life in that place, blind to the patterns I was repeating, deaf to the origins and causes.&lt;/p&gt;
&lt;p&gt;I want to make this series to honor my journey. To relieve myself of the shame and guilt I carry. I want to explore my past, including foundational and key events. I want to talk about how I am healing. I want to be able to look back at myself in a decade and smile in gratitude for having put in the work to realize a more authentic life for myself.&lt;/p&gt;
&lt;p&gt;I'll structure each week on a specific topic or aspect of my recovery. I thought about organizing this chronologically, or thematically, but I think keeping it flexible and letting my story unfold naturally is the right course. To start, then, I have to talk about meetings.&lt;/p&gt;
&lt;h2&gt;Meetings&lt;/h2&gt;
&lt;p&gt;I didn't walk into my first meeting in 28. As a child I grew up attending Alcoholics Anonymous meetings with my father. The impressions that remain are styrofoam cups, hospital meeting rooms, and the serenity prayer. My connection to 12 step pre-dates even this. My parents met at AA, had a brief affair, and produced me. It feels full circle to now attend meetings myself as an adult.&lt;/p&gt;
&lt;p&gt;I first heard of Adult Children of Alcoholics as a teenager. My stepmother &lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; suggested to me that I attend meetings, considering that my birth mother was an alcoholic, my father a recovering alcoholic, and she herself a recovering alcoholic. The irony at the time was that I had no means of independent transportation, seeing as we lived in a rural area without public transit. So, as she did not take me to a meeting, I did not go.&lt;/p&gt;
&lt;p&gt;Fast forward a dozen years, and I am dating someone with whom the relationship proved to be wildly unstable. We had three breakups in the span of six months. After a breakup, I decided to order a copy of a book that I'd seen on her bookshelf: &lt;em&gt;Codependent No More&lt;/em&gt; by Melody Beattie. This book blew me away, it described my exact patterns of behavior in such clear terms that it pierced my denial. I had actually picked up the book in order to understand why my ex was so crazy. Instead it showed how crazy I was, more a mirror into myself than a microscope into another.&lt;/p&gt;
&lt;p&gt;In the book, Melody suggests attending ACoA meetings. I was once again faced with the dilemma of trying to argue myself out of attending. As Groucho Marx said, I don't want to be part of any group that would have me as a member. The name of the group was unarguable, though; I had three alcoholic parents, I am the adult child of them, ergo, I might as well see what it's all about.&lt;/p&gt;
&lt;p&gt;I looked up the meeting list on adultchildren.org, found a nearby meeting, and reached out to the contact person: the meeting says closed, does that mean it's closed to new attendees? Nope, open/closed refers to whether non-identifying people are allowed to attend, i.e. friends, partners, counselors, etc. With much anxiety, I went to my first meeting. I opened my mouth and shared. I came back the next week. And I've been attending every week since, for more than three years now.&lt;/p&gt;
&lt;h2&gt;Fellowship&lt;/h2&gt;
&lt;p&gt;Attending meetings is the bedrock of my recovery. It is the medicine that heals the root of my disease. Weekly attendance, barring travel or sickness, is non-negotiable. Not that I'd want to negotiate anyway, I just love going to meetings. I've met some of the kindest people in the rooms. I've made so many close, trusted friends, that my life feels full of connection, warmth and love now. Contrast that with the past, when I could count my friends on one hand, and couldn't tell you what emotional availability meant.&lt;/p&gt;
&lt;p&gt;Gabor Mate says that authenticity and attachment are the key to recovery. I believe that meetings provide both. In shares, I can express myself, however I like, whatever I like, as much myself as I am able to muster in the moment. With stable meetings, and friendships with fellows, I settle into healthy attachment patterns. I'm able to be myself and be accepted for that. This is a special opportunity, and I treasure it.&lt;/p&gt;
&lt;p&gt;I've worked the steps, I've worked with sponsors, I've chaired meetings and held service positions and carry the message. Recovery is a process, not a finished product, something that Julia Cameron is reminding me this week. So as much as this sounds like a lot to share, it took a long time to shape. At first, I ran out of the door when the meeting ended, not talking to anyone. After six months, I got my first sponsor, and started the steps. My sponsor encouraged me to reach out to other fellows, text and call. I started facing my fear and connecting with others. This slowly blossomed into friendships that lasted. I challenged myself to contribute, to take on responsibilities at meetings, to show up to events. It wasn't comfortable, and it occasionally still isn't, but I know the program works on me as I work on it.&lt;/p&gt;
&lt;p&gt;After a year in ACoA, I started attending an additional fellowship based on relationships and sexuality. This was a missing puzzle piece for me. I had been trying to date again, but my old patterns kept emerging: chasing the wrong people, staying in bad situations, and ignoring red flags. I was able to take a deep look at my dating and relationship behaviors in this fellowship, and it's paid off tremendously: I've been dating my current girlfriend for about ten months. It's been the most sober relationship to date, without the highs and the lows, the fights and break-ups and make-ups, the walking on eggshells, the lack of authenticity or healthy attachment. That's not to say it's been easy, in fact sober dating is even more challenging than unhealthy dating, in part because it feels so uncomfortable, as I'm doing something that was never modeled for me by my parents.&lt;/p&gt;
&lt;h2&gt;Recovery&lt;/h2&gt;
&lt;p&gt;My life is far from perfect today. I am taking a break from work as I heal from burnout. I lack the markers that I associate with success at this age: a house, a family, a career. What I do have is the capability of tapping into inner peace. And I see now that if I had chased those markers, I'd never have found this peace. I'd have been chronically dissatisfied, just like my parents.&lt;/p&gt;
&lt;p&gt;This peace has been hard won. It's taken years of meetings, fellowship, therapy, meditation, journaling, yoga, etc., etc. At one point I estimated my investment in recovery at more than 15 hours per week. The payoff is real, and beyond what I could have imagined - when I first joined, I really only wanted to get back with my ex girlfriend. Now I am opening to my inner world, to myself, rediscovering old dreams and discovering new ones.&lt;/p&gt;
&lt;p&gt;12 Step is the bedrock of my recovery.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Terminology note: I was raised jointly (but separately) by my father and his ex wife, who we continued to refer to as my stepmother after their divorce.&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="recovery"></category></entry><entry><title>Good List Week 12</title><link href="https://blog.tmk.name/2026/06/22/good-list-week-12/" rel="alternate"></link><published>2026-06-22T00:00:00-04:00</published><updated>2026-06-22T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-06-22:/2026/06/22/good-list-week-12/</id><summary type="html">&lt;p&gt;Wow, it's been 3 months of good lists, and I am feeling accomplished for maintaining such consistency in this project. I also feel that the level of adventure is increasing over time, no small thanks to working the &lt;em&gt;Artist's Way&lt;/em&gt; throughout.&lt;/p&gt;
&lt;h2&gt;Mount Tammany&lt;/h2&gt;
&lt;p&gt;Having read about the Delaware Water Gap …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Wow, it's been 3 months of good lists, and I am feeling accomplished for maintaining such consistency in this project. I also feel that the level of adventure is increasing over time, no small thanks to working the &lt;em&gt;Artist's Way&lt;/em&gt; throughout.&lt;/p&gt;
&lt;h2&gt;Mount Tammany&lt;/h2&gt;
&lt;p&gt;Having read about the Delaware Water Gap in &lt;em&gt;A Walk in the Woods&lt;/em&gt;, I beat myself up for having never visited. This week I remedied the matter by hiking Mount Tammany on the &lt;a href="https://www.alltrails.com/trail/us/new-jersey/mount-tammany-red-dot-and-blue-dot-loop-trail?sh=fi47n9&amp;amp;utm_medium=trail_share&amp;amp;utm_source=alltrails_virality"&gt;red and blue trails&lt;/a&gt;. The day started with panic: I became convinced that going in the middle of the week, I'd be the only person on the trail, and therefore guaranteed to be eaten by a vicious bear. I accepted the fear, I reached out to a friend who hikes a lot, and with reassurance in hand, I ventured out west.&lt;/p&gt;
&lt;p&gt;I had been warned the hike was steep, but I had to experience it to understand: the climb up is tough, it's about 1200 feet gain over a mile. I practiced taking breaks, extra breaks, breaks every couple hundred feet. I felt a little like the tortoise in the old story, as I leap-frogged other hikers who pushed through, ultimately beating them to the top.&lt;/p&gt;
&lt;p&gt;The views were spectacular, and I faced even more fear by winding down the rock ledge at the overlook. I had packed a sandwich and coconut water, which tasted all the more delicious with mountain climbing accomplishment for dressing. After some rest, I took the trail back down, which was in some ways more challenging, as the going was rocky the whole way down - I rolled my ankle once, thankfully no more damage than a little soreness the rest of the day. I paused at the stream and falls near the end of the trail, and plopped back into my car and happily drove home - hitting a rainstorm no more than five minutes away! The day really lucked out: clear weather in the 70s, plenty of folks on the trail, no bears (the worst was grasshoppers jumping on me, haha).&lt;/p&gt;
&lt;p&gt;With Mount Tammany hiked, I can mark the Appalachians / Water Gap on my map.&lt;/p&gt;
&lt;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/hikepath.png" class="glightbox" data-gallery="gallery-2" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl12/hikepath.png" alt="Trail elevation"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/peak.jpg" class="glightbox" data-gallery="gallery-2" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl12/peak.jpg" alt="Me on the peak"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/river.jpg" class="glightbox" data-gallery="gallery-2" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl12/river.jpg" alt="The Delaware River"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2&gt;Drumming&lt;/h2&gt;
&lt;p&gt;My girlfriend had suggested we visit a recording studio with drum rentals - I like the percussion of playing drums, and had never in my life sat down to play a drum set! She's an experienced drummer, so with her as instructor and me as mostly attentive student, we went to &lt;a href="https://angrypapaentertainment.com/"&gt;Angry Papa studios&lt;/a&gt; in Wallington, NJ. For $20/hr, we got to jam out to our heart's contents, me trying to get the rhythm of syncing the kick drum with drum sticks, and Maria showing me up with her skills.&lt;/p&gt;
&lt;p&gt;I had a blast, the two hours flew by, I don't think I have any natural talent on the drums, but I am learning that I do like playing music, and can follow a basic beat on an instrument. I next want to get an intro guitar lesson from a friend, as part of a musical tour, before settling down and investing more serious effort into learning one instrument well.&lt;/p&gt;
&lt;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/drumming.png" class="glightbox" data-gallery="gallery-3" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl12/drumming.png" alt="Me on the drums"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h2&gt;Catskills&lt;/h2&gt;
&lt;p&gt;My friend moved to Sullivan County in the Catskills about a year ago, and I dragged my feet on visiting until this past weekend - and how silly do I feel! The area is stunningly beautiful, covered in trees and filled with exciting wildlife. It took about 90 minutes to drive up, a peaceful and easy drive, punctuated by the loss of NPR on 93.9 and poor reception for the Catskills public radio on 90.5. We started at Mamakating Environmental Education Center, or MEEC for short, which is a charming building nestled into Basha Kill Wildlife Refuge. There were binoculars positioned to view woodpeckers nesting in a nearby tree, I was lucky enough to see the baby poking its head out. A local artist is working on a residency project, stitching a mandala with colored layers from the birds observed in the area.&lt;/p&gt;
&lt;p&gt;We then went to Basha Kill proper, an incredible sight, the valley carved out by glaciers some millennia ago. Onward to lunch at Brew in Rock Hill, where I feasted on delicious bbq cauliflower "wings" and an impressive veggie burger. Then to the next adventure, hiking at the Upper Delaware Scenic and Recreational River park, on the lookout for bald eagles. My friend is spectacularly lucky to have bald eagles &lt;em&gt;nesting outside his apartment&lt;/em&gt;! We spotted the baby in the nest, but hoped to find an adult on the Delaware. We spotted a juvenile, maybe one or two years old, feathers not fully developed but white underbelly clear. We soaked our shoes in the water tramping about trying to get a better view.&lt;/p&gt;
&lt;p&gt;There was a troupe of kayakers on the river, and it made me dream of a weekend long kayak and camping trip down the river. Then I thought, how would my back feel sitting in a kayak all day? Well, maybe it'll feel just fine :)&lt;/p&gt;
&lt;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/bashakill.jpg" class="glightbox" data-gallery="gallery-4" data-description="Basha Kill Wildlife Refuge"&gt;&lt;img src="https://blog.tmk.name/images/gl12/bashakill.jpg" alt="Basha Kill"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/delaware2.jpg" class="glightbox" data-gallery="gallery-4" data-description="Upper Delaware Scenic and Recreational River"&gt;&lt;img src="https://blog.tmk.name/images/gl12/delaware2.jpg" alt="Delaware River"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/delaware3.png" class="glightbox" data-gallery="gallery-4" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl12/delaware3.png" alt="Me getting my feet wet"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/eagle.jpg" class="glightbox" data-gallery="gallery-4" data-description="Eagle watching"&gt;&lt;img src="https://blog.tmk.name/images/gl12/eagle.jpg" alt="Me looking at a juvenile eagle"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gl12/delaware4.jpg" class="glightbox" data-gallery="gallery-4" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gl12/delaware4.jpg" alt="Delaware River"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Good List Week 11</title><link href="https://blog.tmk.name/2026/06/14/good-list-week-11/" rel="alternate"></link><published>2026-06-14T00:00:00-04:00</published><updated>2026-06-14T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-06-14:/2026/06/14/good-list-week-11/</id><summary type="html">&lt;p&gt;It was my birthday this week, I celebrated with loving friends and family. I turned 32, thankfully still in my early thirties for the time being (ie still in the "plenty of time to figure it all out!" period of my life, at least I tell myself..). This was my …&lt;/p&gt;</summary><content type="html">&lt;p&gt;It was my birthday this week, I celebrated with loving friends and family. I turned 32, thankfully still in my early thirties for the time being (ie still in the "plenty of time to figure it all out!" period of my life, at least I tell myself..). This was my best birthday in so many years, as I was able to open myself up and feel the good in my life.&lt;/p&gt;
&lt;h2&gt;I Love Boosters&lt;/h2&gt;
&lt;p&gt;I chanced hearing about this movie on the radio, Boots Riley gave an interview about it, helpfully explaining that boosters is a term for shoplifters who resell to the community at discounted prices (in other words, the facebook marketplace resellers of laundry detergent). I had previously seen Sorry to Bother You, so I was excited, but didn't have much in the way of expectations going in. And I am glad I didn't, because Boosters blew me away: it is such a fun, whacky, wild adventure through class, fashion, and labor organizing. I just adored the dialectical materialism raygun, heighten the contradictions indeed, it took me way back to my days as a youthful organizer.&lt;/p&gt;
&lt;h2&gt;A Walk in the Woods&lt;/h2&gt;
&lt;p&gt;Another media hit this week: &lt;em&gt;A Walk in the Woods&lt;/em&gt; by Bill Bryson. I grabbed this and &lt;em&gt;The Wild Parrots of Telegraph Hill&lt;/em&gt; at Montclair Book Center, the premiere used book store in Montclair. I was disappointed to see that they're moving away from used books to higher price/margin new books at an eye watering $18 minimum price, but I scored these two books for less than $10 each.&lt;/p&gt;
&lt;p&gt;It's a trend for me now that I'm reading so many outdoors oriented books: &lt;em&gt;How to Do Nothing&lt;/em&gt;, &lt;em&gt;Into Thin Air&lt;/em&gt;, and now &lt;em&gt;A Walk in the Woods&lt;/em&gt;. It also coincides with my withdrawal from technology and into slowing down and presence. I can't say I don't still get easily spooked and startled outside, whether from a bee or a deer. But I'm in good company with Bryson, who journeyed the Appalachian Trail (AT for short) in the late 90s. I appreciate his sense of humor, as he describes the typical American as someone more likely to drive than walk to their own mailbox, as someone who's experience with nature amounts to little more than driving into the parking lot of a national park, taking a picture, and getting back in the car. I appreciate the irony that as much as Bryson disdains bland, monotonous consumer culture replicating across the country, nothing pleases him more than a hamburger and cheap motel room after just a few days on the trail.&lt;/p&gt;
&lt;p&gt;His walk through history and geography was of particular interest to me, as a New Jersey denizen. I did not realize that the AT is less than an hour drive from me; I also cop to having never visited the Delaware Water Gap despite living so close for so long (I do plan to address this promptly). Bryson's description of nature on the edge, of the total destruction of so many species at record speed (e.g. the American chestnut tree just about eradicated in a few decades, having a population in the billions prior), left an impact on me, in the vein of &lt;em&gt;How to Do Nothing&lt;/em&gt;: I feel disconnected from the earth, from my environment; driving in a car through New Jersey's sprawl feels distinctly uncomfortable now, as do the kept, clean and polished spaces where I spend my time.&lt;/p&gt;
&lt;h2&gt;Boonton Coffee Open Mic Night&lt;/h2&gt;
&lt;p&gt;For my artist date this past week, I went to Boonton Coffee's open mic night, where local musicians and performers play a couple songs or practice their art for the crowd. Boonton Coffee is one of my favorite cafes in the area, a short hop for me on 280/287. Their coffee is high quality, their pastries and toasts tasty, and the space is big and comfortable, with plenty of seating.&lt;/p&gt;
&lt;p&gt;I've been attending open mic nights here for the past couple years, as I'm consistently impressed with the quality of the performers, ranging from hippie dads with an acoustic guitar to up and coming young singers. I also like that the music tends to be more folksy and original, with most musicians opting to play their own music. The star of this most recent show was an eclectic performance by a young artist who danced and sang to what I can only describe as five minutes of scrolling through tiktok, short segments of random songs and audio clips. That she was painted in clown make up and dressed like Chappelle Roan fit the bill.&lt;/p&gt;
&lt;p&gt;After attending so many sessions, I do start to wonder about performing myself, though I have nothing even close to presentable (nor any talent developed enough anyway). Still, with a missed karaoke opportunity this past weekend, my inner teen is expressing the desire to get on stage, to be seen and heard and join in on the fun.&lt;/p&gt;
&lt;h2&gt;Old Photos&lt;/h2&gt;
&lt;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/teen/og.jpg" class="glightbox" data-gallery="gallery-1" data-description="My first profile photo. Note the resolution, did I take this on a flip phone?!"&gt;&lt;img src="https://blog.tmk.name/images/teen/og.jpg" alt="First profile photo"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/teen/youngthug.jpg" class="glightbox" data-gallery="gallery-1" data-description="My first career dream was to become a rapper."&gt;&lt;img src="https://blog.tmk.name/images/teen/youngthug.jpg" alt="Young thug"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/teen/reading.jpg" class="glightbox" data-gallery="gallery-1" data-description="I wonder what book I was reading?"&gt;&lt;img src="https://blog.tmk.name/images/teen/reading.jpg" alt="Reading"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/teen/paris.jpg" class="glightbox" data-gallery="gallery-1" data-description="My siblings and I at the Notre Dame in Paris. Yes I dyed my hair blonde, yes I was a troubled teen."&gt;&lt;img src="https://blog.tmk.name/images/teen/paris.jpg" alt="In Paris"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;I ventured back into the forbidden lands and reactivated my facebook this week. In working with my inner teen, I realized that I don't have any photos of myself as a teenager. But of course facebook does, and I get enough login code emails to be reminded of that. It took a lot of courage, and a couple drinks, but I logged on and revisited all the old memories (curated selection above).&lt;/p&gt;
&lt;p&gt;So many things stood out to me. Was photo quality really so bad back then? I wonder if HD (now 4k)-by-default plays a part in technology vampirizing our attention. The way we used to post full photo albums, I remember, with a digital camera and just bulk uploading fifty or a hundred photos at once. No taking the perfect picture, cropping, editing, etc etc, just dumping what you had and moving on. And that we'd look at albums of other people! Now it's a chore to look at one photo sent from a friend. I was also amazed at how innocent the internet was back just ten to fifteen years ago: adding random people as friends, posting on public walls, status updates ranging from banal to banal.&lt;/p&gt;
&lt;p&gt;What hit me most was seeing myself as a teenager again. I think that was my block these past years against doing so. I've long carried so much guilt and shame about my teenage years, feeling like a failure. In my healing, I started with my the basics with my inner teen, then worked with my inner child's wounds, and now am back working with the deep wounds of my inner teen. Some pictures are shocking: I can't believe how much pain is written into my face, my posture. Sure, there's gawkiness and angst, but then there's wounds, layered and layered.&lt;/p&gt;
&lt;p&gt;In my teenage self, I see so much longing, longing for relief, for acceptance, for guidance. As my teenage years progressed, unfortunately, my home life deteriorated until I was homeless, sleeping on a friend's couch. I had to shut down so much of myself to survive. I see now my bravery, ingenuity, creativity, resourcefulness in my inner teen. Rebuilding my relationship with my inner family will take time, but I recognize the immense progress in this action, and feel the release.&lt;/p&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Good List Week 10</title><link href="https://blog.tmk.name/2026/06/08/good-list-week-10/" rel="alternate"></link><published>2026-06-08T00:00:00-04:00</published><updated>2026-06-08T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-06-08:/2026/06/08/good-list-week-10/</id><summary type="html">&lt;h2&gt;Birthday hike and swim&lt;/h2&gt;
&lt;p&gt;Last year, I had planned to host friends at a nearby park for an outdoor picnic on my birthday. Due to rain, the party moved indoors to my apartment, where we crowded around the living room and played pictionary. This year, I wanted to up the …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Birthday hike and swim&lt;/h2&gt;
&lt;p&gt;Last year, I had planned to host friends at a nearby park for an outdoor picnic on my birthday. Due to rain, the party moved indoors to my apartment, where we crowded around the living room and played pictionary. This year, I wanted to up the ante; I jokingly said I wanted a birthday that folks would remember. I think I accomplished that with a hike and swim at Harriman State Park. We roughly followed this &lt;a href="https://www.alltrails.com/explore/trail/us/new-york/pine-meadow-lake-via-7-hills-trail?u=i&amp;amp;sh=anply3"&gt;Pine Meadow Lake trail&lt;/a&gt;, making an extra stop at Lake Wanoksink first, before making camp at Pine Meadow Lake. When we arrived, we were warned of a giant snapping turtle in the water; some folks (me included) stayed at the water's edge, while others merrily swam out. Some folks napped and sunned, while others munched away at their trail mix and snacks.&lt;/p&gt;
&lt;p&gt;By the time we got back, and made it to the restaurant (Characters in Sloatsburg - great stopover for traditional American fare), we were all exhausted, thirsty and starving. Massive plates of food were consumed, or attempted to be consumed. The drive home was peaceful and quiet on the Jersey Turnpike, as we let the accomplishment sink in and our bodies rest.&lt;/p&gt;
&lt;p&gt;I personally had a positive but challenging time. Last year as this year, I found myself focused on everyone but myself, are they having a good time, do they have enough food and water, is everyone comfortable and socializing. All the same, progress is progress, and my shoulders were elevated but not nearly as high this time as last. I found myself also once again overwhelmed by how many people showed up, there were 14 of us all together, just an incredible display of love and connection that I cherish.&lt;/p&gt;
&lt;p&gt;&lt;img alt="lake guardian" src="https://blog.tmk.name/images/lake-guardian.jpg" /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;While some of us doubted, the turtle turned out to be very real, and very large.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Comedy show&lt;/h2&gt;
&lt;p&gt;I love live performances and theater, and I love comedy, so when my stand-up comic friend invited me to her show I jumped at the chance. The crowd reminded me of college: loud, boisterous friends leaning over to greet each other; cackling laughs from select audience; my seat neighbor finishing off five cans of beer. The comics were fantastic, all local, really hyperlocal, as the headliner grew up nearby and shared his story of buying condoms for the first time in the same place that he now performed (Lackawanna Plaza used to host a Pathmark supermarket).&lt;/p&gt;
&lt;h2&gt;Ramapo artist date&lt;/h2&gt;
&lt;p&gt;For my artist's date this week, I listened to my inner teen and leaned into "expansiveness" in time and space. I realized that I orient my time around responsibilities, and try to compartmentalize play. My inner teen can't settle into a fixed hour of time set aside; he craves long drives to far off places, whole afternoons and evenings without a care in sight. I recalled that as a teenager I liked to write sappy poetry - as cringe as it is now, I'm going to link to my &lt;a href="https://russon77.blogspot.com/"&gt;teenage poetry blog&lt;/a&gt;. I had a desire to revisit Ramapo Valley County Reservation, particularly the waterfall. I stopped at the store and bought myself a little black notebook (yes, an &lt;a href="https://www.youtube.com/watch?v=CM-N9d8cm-A"&gt;homage to Pink Floyd&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I then took the long drive to Mahwah, listening to The Wall. I hiked up to MacMillan Reservoir, a significantly steeper venture than I had recalled. I was surprised by a frog, and watched a dog playfully splash about in the water. I then hiked down, and stopped where I could sit on a rock in the rushing brook. I breathed in the fresh air, dancing light, and running water. I wrote poetry. Then I hiked down to the waterfall, where I sat in meditative quiet, admiring all the ways the water effortlessly snakes down the rocks.&lt;/p&gt;
&lt;p&gt;&lt;img alt="ramapo waterfall" src="https://blog.tmk.name/images/hikes/ramapo-waterfall.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Artist's Way Week 4</title><link href="https://blog.tmk.name/2026/06/02/artists-way-week-4/" rel="alternate"></link><published>2026-06-02T00:00:00-04:00</published><updated>2026-06-02T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-06-02:/2026/06/02/artists-way-week-4/</id><summary type="html">&lt;p&gt;I started the Artist's Way a month ago, seeking to get in touch with my inner child/inner artist, to learn to play and be more spontaneous. So far, so good, as I've started to open myself up to more of the good in the world. I don't buy into …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I started the Artist's Way a month ago, seeking to get in touch with my inner child/inner artist, to learn to play and be more spontaneous. So far, so good, as I've started to open myself up to more of the good in the world. I don't buy into the "benevolent god" idea of a Higher Power, and that's long been a point of some contention for me in 12 Step. This book is helping me to better understand my own idea about good and Higher Power: there is good and bad in the world, in my closed off / victimized / solipsistic / narcissistic / ego-driven state I can't see nor access the good, but as I practice opening up / embracing the discomfort I see there is good, or at least opportunity for good in the world. As Julia says (we are on a first name basis, I don't think she'd mind), I still have to do the footwork, it's just that, as a monk shared last weekend, rather than walking in a circle blind to repeating the same patterns, I'm starting to notice and have the courage to walk new, unknown, uncertain paths.&lt;/p&gt;
&lt;p&gt;Speaking of discomfort, I did not know to expect the week 4 media deprivation challenge. However, it came about with some serendipity: all of my tv shows ended their seasons the week before (SNL, 60 Minutes, The Boys). I therefore entered the week with courage and slightly less FOMO.&lt;/p&gt;
&lt;p&gt;As the book was written in the 90s, I sought out others' perspectives on a modern take on reading deprivation. It seemed the idea remained the same, just with updated tactics: stop taking in others' ideas, giving my own inner voice room to speak. Practically, that meant:&lt;/p&gt;
&lt;p&gt;No:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TV, movies, video games (I unplugged my TV for the week)&lt;/li&gt;
&lt;li&gt;Reading books, checking the news, browsing social media (I put a 0 minute app timer on most apps so they'd be inaccessible by default)&lt;/li&gt;
&lt;li&gt;Music, radio, podcasts&lt;/li&gt;
&lt;li&gt;AI tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Minimal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Computer, acceptable usage were checking emails twice a day, medical information, paying bills&lt;/li&gt;
&lt;li&gt;Texting, instead preferring calls and in person meetups&lt;/li&gt;
&lt;li&gt;Phone, acceptable usage including maps, phone calls, planning events &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Solitaire&lt;/li&gt;
&lt;li&gt;Cleaning my apartment&lt;/li&gt;
&lt;li&gt;Exercising&lt;/li&gt;
&lt;li&gt;Writing&lt;/li&gt;
&lt;li&gt;Meditation&lt;/li&gt;
&lt;li&gt;Walks&lt;/li&gt;
&lt;li&gt;Coffee with friends&lt;/li&gt;
&lt;li&gt;Quiet time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I am proud to say that I survived the full week, seven full days (Sunday midnight to Sunday midnight) following these rules. From a small sample size, I got the vibe that others generally did not take this as seriously as I did, whether skipping, or limiting instead of eliminating media consumption. That's not to say that some don't take it even more seriously, one person's experience included no non-critical texting nor reading (not even restaurant menus!). I am satisfied with my ruleset, and the proof for me in my experience is the widened perspective I gained.&lt;/p&gt;
&lt;p&gt;I was most excited about dropping distractions when eating, on the toilet, before bed, and when tired. I've long tried to embrace the discomfort of just doing the activity itself, with myself, but could not make it more than a couple days before the old distractions returned. And I will readily admit, I experienced a lot of discomfort last week. I felt it most apparent in my body, specifically in my back, as it remained tense for most of the week. I took the opportunity to accept the discomfort, sit with it, help it to move with yoga, breathing exercises, cold exposure, running. Nothing eliminated the discomfort, and I was surprised to note after the first few days that part of me started to lean into the slower pace of stimulation, even as another part felt stressed.&lt;/p&gt;
&lt;p&gt;It was helpful working with my therapist to see that my inner teen felt the tech deprivation specifically to be a punishment. Growing up, I was stuck at home in a rural area, so I leaned on technology (video games, social media) to escape a chaotic home life. Pulling back on tech last week, my inner teen felt that he was losing something, or having something taken away; this helped me to understand some of the tension, and start to think about how I could integrate technology in a way that affirms this part of my life, without becoming a compulsive distraction that curtails my current life.&lt;/p&gt;
&lt;p&gt;A key insight of my experience was that chronic distraction doesn't alleviate anxiety, it actually reproduces it. That's to say, as I reduced stimulation, I also craved it less. Time started to flow differently, suddenly slower paced activities didn't seem like such a drag - for example, running without headphones (I noticed I was the only person doing so!) I felt the run go by significantly quicker than in the past when I suffered through each footfall music blaring in my ears. Long drives without radio or music went by quickly. My meditations were much better, I experienced dropping into a quiet and calm state much quicker and easier than usual. I also found myself craving more time on the cushion, longer sessions, trying out seldom used approaches like progressive muscle relaxation at night.&lt;/p&gt;
&lt;p&gt;I also noticed my cognition and memory improved, quite drastically: suddenly I could remember all of the events of the previous day. Over time, I felt my inner voice start to speak up, and it was like greeting an old friend who had slowly faded away some long time ago. I am gifted with a powerful and creative mind, but for some time it's been relegated to passive observer instead of active participant. It's special to get back in touch with myself in this way.&lt;/p&gt;
&lt;p&gt;My journey to a full tech detox did not start last week, but goes back years. I knew that I had an unhealthy relationship, and started tapering off maybe three or so years ago, in successive year-long rounds. The first round of tapering was eliminating the "worst of the worst" of social media, video games and pornography usage. The next year, the "less bad" social media. And then I was reduced to mostly browsing the news and wikipedia. So the background for a full week detox paints a comprehensive picture of a multi year journey to radically reshape my relationship with technology.&lt;/p&gt;
&lt;p&gt;Moving forward, my plan is to continue with tech-free mornings, meals, nights. I'll be flexible, I'm not going to deny myself a Saturday morning movie if I am really excited about it (that's the fun and play, as opposed to self-deprivation mindset). Even two days after ending my fast, I haven't watched tv nor turned on the radio in the car. I feel the slower pace working through me, fiber by fiber, and I'm curious to see what more time will reveal.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Good List Week 9</title><link href="https://blog.tmk.name/2026/06/01/good-list-week-9/" rel="alternate"></link><published>2026-06-01T00:00:00-04:00</published><updated>2026-06-01T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-06-01:/2026/06/01/good-list-week-9/</id><summary type="html">&lt;h2&gt;Memorial Day&lt;/h2&gt;
&lt;p&gt;For Memorial Day, I co-organized a hike and barbeque with friends: about twenty people showed up, the grill was manned, food was shared, wiffle ball was played (as team captain I drafted to win, my competitive spirit got the best of my sportsmanship). With the weather uncertain, we …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Memorial Day&lt;/h2&gt;
&lt;p&gt;For Memorial Day, I co-organized a hike and barbeque with friends: about twenty people showed up, the grill was manned, food was shared, wiffle ball was played (as team captain I drafted to win, my competitive spirit got the best of my sportsmanship). With the weather uncertain, we were able to grab multiple tables and a grill without competition; we returned to Watchung Reservation for the second year.&lt;/p&gt;
&lt;h2&gt;Blood Drive&lt;/h2&gt;
&lt;p&gt;I invited a new friend to meet for coffee, and he mentioned that we'd have to plan around a blood drive he was participating in. Undeterred, I suggested I join him for the blood drive; it's been years since I donated blood. It felt good to be of service, I was thinking of the phrase, "to feel esteem, do esteemable acts." I did end up almost fainting, having forgotten that I get shaky with the blood loss; the kind attendants resurrected me and I left with a whole box of girl scout cookies (samoas!).&lt;/p&gt;
&lt;h2&gt;Pickle Ball&lt;/h2&gt;
&lt;p&gt;One symptom of my progress with the Artist's Way is trying new activities. My friend had been trying to plan a pickleball match with a larger group, so I suggested we get some cheap paddles and play ourselves in the meantime. A trip to Target and $20 later, I was in possession of a set of paddles. As we arrived at the court, the group playing walked off, serendipitous indeed. We ended up playing with another pair that showed up, and got our butts kicked - all in good fun, considering we had twenty minutes of experience prior to our first match!&lt;/p&gt;
&lt;p&gt;&lt;img alt="pickleball" src="https://blog.tmk.name/images/pickleball.jpg" /&gt;&lt;/p&gt;
&lt;h2&gt;Woodstock, NY&lt;/h2&gt;
&lt;p&gt;As I've been experimenting (playing?) with music and instruments, I've talked about getting some basic drums, either a drum pad or bongo drums. My girlfriend suggested we take a day trip to Woodstock, NY (not to be confused with the festival, though you wouldn't know by visiting the town), where there's a drum circle on Sunday evenings from 4-6pm. The town is really cute, there were so many clever store names in the area (e.g. Candlestock). We ate at the Garden Cafe, an all vegan restaurant, which was just delectable.&lt;/p&gt;
&lt;p&gt;Finally, we joined the drum circle, they provide all the instruments and chairs, and I'd guess the crowd was about thirty or forty people total, all drumming along together. I liked it, but I realized that I struggled to really sink into the music, as the drum circle was more chaotic than ordered: there was no one beat that everyone played to, so it was more a joining of many different rhythms to form one song. In contrast, I feel I can sink into a single, steady rhythm that all are tuned to.&lt;/p&gt;
&lt;p&gt;&lt;img alt="drum circle" src="https://blog.tmk.name/images/drumcircle.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Good List Week 8</title><link href="https://blog.tmk.name/2026/05/24/good-list-week-8/" rel="alternate"></link><published>2026-05-24T00:00:00-04:00</published><updated>2026-05-24T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-05-24:/2026/05/24/good-list-week-8/</id><summary type="html">&lt;h2&gt;Kite flying&lt;/h2&gt;
&lt;p&gt;For my artist date this week, I bought a kite at the toy store and went to the local park to fly it. It is funny to me that I just read Julia Cameron share on synchronicity and serendipity. I first went to Target, assured by Claude that …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Kite flying&lt;/h2&gt;
&lt;p&gt;For my artist date this week, I bought a kite at the toy store and went to the local park to fly it. It is funny to me that I just read Julia Cameron share on synchronicity and serendipity. I first went to Target, assured by Claude that they'd have kites in stock. The store worker looked at me funny and suggested I order one on Amazon. Not to be deterred, I called a nearby toy store and they were stocked! The weather that day (Tuesday) chanced to be the last beautiful day for some time (it's rained every day since), at a gorgeous mid nineties with just the right breeze.&lt;/p&gt;
&lt;p&gt;Flying the kite, I was taken back to memories from childhood, flying kites at the beach, and at a nearby park from my home. I could recall the string spinny handler thing rolling in my hands; the kite diving into the water, to be resurrected back into the sky.&lt;/p&gt;
&lt;p&gt;&lt;img alt="kite flying in the air" src="https://blog.tmk.name/images/kite-flying.jpg" /&gt;&lt;/p&gt;
&lt;h2&gt;Presby Iris Gardens&lt;/h2&gt;
&lt;p&gt;Driving through Montclair, I passed the Presby Iris Gardens, and noticed cars parked along the side and people walking among the flowers. With a tinge of spontaneity, I went back later that day, and stopped to &lt;em&gt;smell the flowers&lt;/em&gt;, a mix of sweet, earthy, candy, bitter. I must've driven by this park dozens of times, plenty during in spring season, and yet have never visited this delight before.&lt;/p&gt;
&lt;p&gt;&lt;img alt="flower" src="https://blog.tmk.name/images/presby-garden.jpg" /&gt;&lt;/p&gt;
&lt;h2&gt;Running&lt;/h2&gt;
&lt;p&gt;After reading &lt;em&gt;Into Thin Air&lt;/em&gt; by Jon Krakauer, I (presumably in good company) became very interested in mountaineering. In order to manage my expectations and build up to Everest, I set a modest goal of running a 5k. With serendipity, there is a local race six weeks away, plenty of time to train. I went to my local thrift shop to pick up some running shorts, and they had the exact kind I was looking for, at a steep discount.&lt;/p&gt;
&lt;p&gt;I have hit the track at Brookdale Park twice so far: 1 mile, 1.25 miles. I'm following a modified couch to 5k schedule, reformulated for a track (eg 1 lap run, 1 lap walk) rather than time (30s run, 60s walk).&lt;/p&gt;
&lt;p&gt;It's taken me back to the year I ran track in high school. I also ran a 5k in college. I remember running a mile or so maybe six years back or so. I've long had a difficult relationship with running: I didn't feel comfortable in my body, the internal pressure to achieve was more harm than help. This time around, with about a year of yoga under my belt, and a much better relationship with myself, these training sessions have been, well, surprisingly easy.&lt;/p&gt;
&lt;h2&gt;Veggie points&lt;/h2&gt;
&lt;p&gt;This was a fun one, my girlfriend suggested that we log each unique vegetable that we eat for one week. The catch is that the veggies should not be processed in order to count (we made exceptions for minimally processed foods like peanut butter and hummus). The target was 30 unique veggies, which we both hit! As vegetarians it wasn't particularly challenging, and I'd say I didn't make any special effort to consume new veggies for the list.&lt;/p&gt;
&lt;p&gt;&lt;img alt="veggie points list" src="https://blog.tmk.name/images/veggie-points.png" /&gt;&lt;/p&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Meditation Misconceptions Part 2</title><link href="https://blog.tmk.name/2026/05/22/meditation-misconceptions-part-2/" rel="alternate"></link><published>2026-05-22T00:00:00-04:00</published><updated>2026-05-22T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-05-22:/2026/05/22/meditation-misconceptions-part-2/</id><summary type="html">&lt;p&gt;&lt;a href="https://blog.tmk.name/2023/12/31/meditation-misconceptions/"&gt;After two and a half years&lt;/a&gt;, I wanted to share some new insights into meditation.&lt;/p&gt;
&lt;h2&gt;Meditation is just sitting&lt;/h2&gt;
&lt;p&gt;Most of my past meditation practice was simple: just sit on the cushion. A Buddhist monk shared that it's common to practice gentle movement before, however, maybe walking meditation, or yoga …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://blog.tmk.name/2023/12/31/meditation-misconceptions/"&gt;After two and a half years&lt;/a&gt;, I wanted to share some new insights into meditation.&lt;/p&gt;
&lt;h2&gt;Meditation is just sitting&lt;/h2&gt;
&lt;p&gt;Most of my past meditation practice was simple: just sit on the cushion. A Buddhist monk shared that it's common to practice gentle movement before, however, maybe walking meditation, or yoga. Walking was my first foray into pre-sitting movement, trying the monk's suggestion of balancing the time at 10 minutes each of walking, then sitting. The results were limited for me, I found it hard to concentrate during the walking portion, and not particularly relaxed once on the cushion.&lt;/p&gt;
&lt;p&gt;This year, having been practicing yoga, I experimented with 5-10 minutes of yoga instead, and have found it effective. The basic idea is that I'm carrying a lot of stress and tension in my body, and without an outlet, sitting on the cushion will bring that stress and tension with me. For me, movements involving my hips, back, and shoulders are highly effective at easing that tension, such that once on the mat, I'm able to ease into the meditation much easier.&lt;/p&gt;
&lt;p&gt;I think of this as  &lt;em&gt;calming before&lt;/em&gt; meditation.&lt;/p&gt;
&lt;h2&gt;Meditation is just watching the breath&lt;/h2&gt;
&lt;p&gt;This one caught me by surprise, after spending hours on the cushion watching myself breathe shallowly: it's not against the practice to guide my breath, especially when I'm in an elevated state of stress. This misconception relates to the above, in that meditation is allowed to be a calming experience. Not that sitting with stress and discomfort isn't part of the practice, but I think of it as bringing myself out of the overwhelm zone and back into the challenge zone.&lt;/p&gt;
&lt;p&gt;Some easy techniques include 4-7-8 breathing (for calming) and box breathing (for regulation and focus).&lt;/p&gt;
&lt;p&gt;I think of this as &lt;em&gt;calming during&lt;/em&gt; meditation.&lt;/p&gt;
&lt;h2&gt;Meditation is torture&lt;/h2&gt;
&lt;p&gt;Related to the previous, I spent hours trying to meditate in a state of heightened anxiety bordering on panic. I'd get through to the end, up to 45 minutes, and not feel any better for it; sometimes I'd feel exhausted afterward. The simple truth of the matter is that meditation does not need to be torture. If I'm in a panicked state, then silent meditation may not be the right call; that's not failure, that's meeting myself where I'm at.&lt;/p&gt;
&lt;p&gt;The story the monks like to tell is of the king's chef. The first chef makes the same meal every day, despite the king's protests, inclinations, and moods. This chef does not last long in the king's court. The next chef, however, has a keen eye, and closely monitors the king before, during, and after each meal. This chef cooks the right meal for the right moment, and the king is delighted.&lt;/p&gt;
&lt;p&gt;I think of this as &lt;em&gt;choosing the right tool&lt;/em&gt;. It may look like swapping meditation for another activity: yoga, walking, socializing. I notice that after these, I feel lighter and calmer.&lt;/p&gt;</content><category term="reflections"></category></entry><entry><title>Good List Week 7</title><link href="https://blog.tmk.name/2026/05/18/good-list-week-7/" rel="alternate"></link><published>2026-05-18T00:00:00-04:00</published><updated>2026-05-18T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-05-18:/2026/05/18/good-list-week-7/</id><summary type="html">&lt;h2&gt;Turtleback Zoo&lt;/h2&gt;
&lt;p&gt;My inner child loves animals, but I've long been resistant to visiting the zoo: animals in pens and cages, isolated and away from their natural habitat. I recognized those thoughts as &lt;em&gt;adult&lt;/em&gt; discomforts, and challenged myself to take my inner child to the zoo anyway. It went well …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Turtleback Zoo&lt;/h2&gt;
&lt;p&gt;My inner child loves animals, but I've long been resistant to visiting the zoo: animals in pens and cages, isolated and away from their natural habitat. I recognized those thoughts as &lt;em&gt;adult&lt;/em&gt; discomforts, and challenged myself to take my inner child to the zoo anyway. It went well, standing by the cows my inner child wanted to hug their furry bodies and befriend them. This zoo has a wide array of animals, though not all were exhibiting themselves: alligator, komodo dragon, snakes, penguins, birds of prey, hyena, flamingos, gibbons, giraffes (unseen included the lion and bear).&lt;/p&gt;
&lt;p&gt;I remembered that I grew up near the Cohanzick Zoo, going there as a small child, and passing by often over the years, walking, biking, driving. Zoos have a deeper meaning for my inner child, and I am thankful for the courage to explore that meaning.&lt;/p&gt;
&lt;p&gt;&lt;img alt="zoo map" src="https://blog.tmk.name/images/zoo.png" /&gt;&lt;/p&gt;
&lt;h2&gt;Mills Reservation&lt;/h2&gt;
&lt;p&gt;Another nearby spot that I've managed to miss in the several years I've lived in Essex County - Mills Reservation is a delightful wooded park with trails and an overlook looking over New Jersey and New York City. We had perfect spring weather, and when a friend invited me, I jumped at the chance to get outdoors in company. She's big into outdoors meditation, sitting in the park, listening to the breeze rustle the trees, the people, the dogs, the birds, I realized that outdoor meditation had been a goal of mine and by opening myself to saying "yes" I found myself exactly where I wanted to be.&lt;/p&gt;
&lt;h2&gt;Ridgewood&lt;/h2&gt;
&lt;p&gt;I'm a fan of the library system here in New Jersey, and the dozens of libraries part of the BCCLS system. I pasted the list of libraries into claude and was recommended to visit Ridgewood library, so I ventured out on the parkway north to the unvisited town of Ridgewood. It is charming, very similar to Montclair (in fact, many of the same restaurants haha), except with my main gripe resolved: a quaint park in the center of town, with benches and space to just sit and be. I opted for Korean for dinner, and was pleasantly surprised by the quality.&lt;/p&gt;
&lt;p&gt;I spent more time in town than expected, so by the time I got to the library I was in a bit of a hurry. Still, I took time to absorb it, and found it to be a classic library: history displays, art displays, a seed sharing program, a reading room, and floors of rows and rows of books. I found the book I was seeking, &lt;em&gt;Into Thin Air&lt;/em&gt; by Jon Krakauer - I had heard him on NPR last week - and said til next time to Ridgewood.&lt;/p&gt;
&lt;p&gt;Side note: the &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt; has been updated to include these new entries.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Red Lentil Curry</title><link href="https://blog.tmk.name/2026/05/13/red-lentil-curry/" rel="alternate"></link><published>2026-05-13T00:00:00-04:00</published><updated>2026-05-13T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-05-13:/2026/05/13/red-lentil-curry/</id><summary type="html">&lt;p&gt;I've wanted to document my recipes somewhere for the longest time and I am finally ready to sit down and do so. I am a vegetarian and tend towards simpler one pot meals that I can batch prep (curries, chilis, etc). I'll spare the long intro and jump into the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've wanted to document my recipes somewhere for the longest time and I am finally ready to sit down and do so. I am a vegetarian and tend towards simpler one pot meals that I can batch prep (curries, chilis, etc). I'll spare the long intro and jump into the recipe.&lt;/p&gt;
&lt;div class="recipe not-prose my-6 rounded-xl border border-gray-200 bg-gray-50 p-6 shadow-sm"&gt;
  &lt;div class="grid grid-cols-1 gap-8 md:grid-cols-2"&gt;
    &lt;section&gt;
      &lt;h3 class="mb-2 text-base font-semibold text-gray-800"&gt;Ingredients&lt;/h3&gt;
      &lt;p class="mb-3 text-sm text-gray-500"&gt;Makes 4 servings&lt;/p&gt;
      &lt;ul class="space-y-1 text-sm text-gray-700"&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 russet potato, cubed&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 small onion, diced&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 red bell pepper, diced&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 cup red lentils&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 cup spinach (sub frozen kale)&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;3 garlic cloves&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;2 cups vegetable stock&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 cup water&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 medium carrot&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 can coconut milk&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 tbsp red chili powder&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1 tbsp curry powder&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;1/2 tsp ginger&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;Oil (olive or coconut)&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;Salt and pepper to taste&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;Yoghurt&lt;/span&gt;&lt;/li&gt;&lt;li class="flex items-baseline gap-2"&gt;&lt;span class="text-gray-400 flex-shrink-0"&gt;&amp;#8226;&lt;/span&gt;&lt;span&gt;Naan, pita or sourdough bread&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;
    &lt;/section&gt;
    &lt;section&gt;
      &lt;h3 class="mb-3 text-base font-semibold text-gray-800"&gt;Steps&lt;/h3&gt;
      &lt;ol class="space-y-3 text-sm text-gray-700"&gt;&lt;li class="flex gap-3"&gt;&lt;span class="flex-shrink-0 font-semibold text-gray-400"&gt;1.&lt;/span&gt;&lt;span&gt;Heat oil in pot. Add garlic, onion, carrot. Cook for a few minutes.&lt;/span&gt;&lt;/li&gt;&lt;li class="flex gap-3"&gt;&lt;span class="flex-shrink-0 font-semibold text-gray-400"&gt;2.&lt;/span&gt;&lt;span&gt;Stir in spices. Cook for one minute.&lt;/span&gt;&lt;/li&gt;&lt;li class="flex gap-3"&gt;&lt;span class="flex-shrink-0 font-semibold text-gray-400"&gt;3.&lt;/span&gt;&lt;span&gt;Add lentils, potato, vegetable stock, and water. Bring to boil, let simmer for 10 minutes.&lt;/span&gt;&lt;/li&gt;&lt;li class="flex gap-3"&gt;&lt;span class="flex-shrink-0 font-semibold text-gray-400"&gt;4.&lt;/span&gt;&lt;span&gt;Stir in coconut milk and spinach. Generously add salt and pepper. Return to boil and simmer for 5 minutes.&lt;/span&gt;&lt;/li&gt;&lt;li class="flex gap-3"&gt;&lt;span class="flex-shrink-0 font-semibold text-gray-400"&gt;5.&lt;/span&gt;&lt;span&gt;Serve in bowl with heap of yoghurt and bread.&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;
    &lt;/section&gt;
  &lt;/div&gt;
  &lt;section class="mt-6 border-t border-gray-200 pt-4"&gt;&lt;h3 class="mb-2 text-base font-semibold text-gray-800"&gt;Notes&lt;/h3&gt;&lt;p class="text-gray-700 text-sm"&gt;This is a flexible recipe: potato can be dropped, onion and peppers can be frozen (when cooking frozen veggies, the trick is to heat just the veggies with no oil yet, letting the water boil off before adding oil), spices can be pretty much any mix of traditional curry spices. Swapping the lentils for chickpeas would also work, maybe with less liquid volume.&lt;/p&gt;&lt;/section&gt;
&lt;/div&gt;</content><category term="recipes"></category></entry><entry><title>Good List Visualized</title><link href="https://blog.tmk.name/2026/05/12/good-list-visualized/" rel="alternate"></link><published>2026-05-12T00:00:00-04:00</published><updated>2026-05-12T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-05-12:/2026/05/12/good-list-visualized/</id><summary type="html">&lt;p&gt;As I've been writing the good list series of posts, I've been interested in visualizing the activities on a map. I could've created a shared google maps list, but I wanted to do something a little more bespoke, specifically around the design. I also find the google maps lists to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As I've been writing the good list series of posts, I've been interested in visualizing the activities on a map. I could've created a shared google maps list, but I wanted to do something a little more bespoke, specifically around the design. I also find the google maps lists to be cluttered; my default maps view is filled with pins.&lt;/p&gt;
&lt;p&gt;So I spent an hour with claude code this morning converting the lists into usable shape and rendering on a beautiful watercolor inspired map from Stadia / Stamen:&lt;/p&gt;
&lt;p&gt;&lt;img alt="good list visualizer screenshot" src="https://blog.tmk.name/images/glv-screenshot.png" /&gt;&lt;/p&gt;
&lt;p&gt;I think it came out quite well. I find the visualization to add an extra dimension, well, quite literally of space.&lt;/p&gt;
&lt;p&gt;Check out the live site here: &lt;a href="https://blog.tmk.name/apps/glv.html"&gt;Good List Visualizer&lt;/a&gt;.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Good List Week 6</title><link href="https://blog.tmk.name/2026/05/11/good-list-week-6/" rel="alternate"></link><published>2026-05-11T00:00:00-04:00</published><updated>2026-05-11T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-05-11:/2026/05/11/good-list-week-6/</id><summary type="html">&lt;h2&gt;Montclair Art Museum&lt;/h2&gt;
&lt;p&gt;The Montclair Art Museum hosts a free first Thursdays event monthly, open in the evening. There's activities and crafts, live performances, and free guided tours.&lt;/p&gt;
&lt;p&gt;There's currently an exhibit on the American landscape, including classic works by Hudson River School painters Thomas Cole and Thomas Moran.&lt;/p&gt;
&lt;p&gt;&lt;img alt="gathering hay in the salt marshes" src="https://blog.tmk.name/images/gatheringhayinsaltmarshes.png" /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gathering …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;h2&gt;Montclair Art Museum&lt;/h2&gt;
&lt;p&gt;The Montclair Art Museum hosts a free first Thursdays event monthly, open in the evening. There's activities and crafts, live performances, and free guided tours.&lt;/p&gt;
&lt;p&gt;There's currently an exhibit on the American landscape, including classic works by Hudson River School painters Thomas Cole and Thomas Moran.&lt;/p&gt;
&lt;p&gt;&lt;img alt="gathering hay in the salt marshes" src="https://blog.tmk.name/images/gatheringhayinsaltmarshes.png" /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gathering Hay in the Salt Marshes by Martin Johnson Heade&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Enter the Dragon&lt;/h2&gt;
&lt;p&gt;Montclair film occasionally puts on special screenings, this past week showing martial arts classic Enter the Dragon. There was a pre-show demonstration by a local martial arts group, providing some context to the movie; though the movie really stands on its own, with epic fight scenes and dialog.&lt;/p&gt;
&lt;p&gt;&lt;img alt="bruce lee" src="https://blog.tmk.name/images/brucelee.jpg" /&gt;&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Cloudflare Email Workers</title><link href="https://blog.tmk.name/2026/05/05/cloudflare-email-workers/" rel="alternate"></link><published>2026-05-05T00:00:00-04:00</published><updated>2026-05-08T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-05-05:/2026/05/05/cloudflare-email-workers/</id><summary type="html">&lt;p&gt;I have tried various todo / personal task management apps over the years, and I get all excited setting them up with categories and add-ons and integrations only to find that I don't even look at the app a month later. Blargh.&lt;/p&gt;
&lt;p&gt;One of the methods that has stuck with me …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have tried various todo / personal task management apps over the years, and I get all excited setting them up with categories and add-ons and integrations only to find that I don't even look at the app a month later. Blargh.&lt;/p&gt;
&lt;p&gt;One of the methods that has stuck with me is emailing myself. It's not too surprising - an email inbox is pretty much a flexible todo list. So I wanted to try meeting myself where I'm at, by hooking into the email habit programmatically to build a personalized todo app.&lt;/p&gt;
&lt;p&gt;Building a todo app is a fairly tedious crud exercise, but programmatic email access was new to me. For my domain, tmk.name, I use cloudflare to forward emails to my personal account. I was excited to discover that cloudflare supports connecting their worker product with an email trigger:&lt;/p&gt;
&lt;p&gt;&lt;img alt="cf email setup" src="https://blog.tmk.name/images/cloudflare/email-worker-setup.png" /&gt;&lt;/p&gt;
&lt;p&gt;In my use case, I wanted to forward the email via webhook to my backend. Editing the worker code in the browser was suitable for such a small snippet of 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;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;export&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;default&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;async&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; email(message, env, ctx) {&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;// message.raw is a ReadableStream, needs to be converted to a string&lt;/span&gt;
&lt;span style="color: #d4d2c8"&gt;    &lt;/span&gt;&lt;span style="color: #7e8aa1"&gt;// see https://developers.cloudflare.com/email-routing/email-workers/runtime-api/#forwardableemailmessage-definition&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; 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: #FFAD66"&gt;await&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; Response(message.raw).text();&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(env.WEBHOOK_URL, {&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;quot;POST&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: #d4d2c8"&gt;        &lt;/span&gt;&lt;span style="color: #D5FF80"&gt;&amp;quot;Content-Type&amp;quot;&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;application/json&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;X-Webhook-Secret&amp;quot;&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; env.WEBHOOK_SECRET&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: #FFD173"&gt;JSON&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;.stringify({ &lt;/span&gt;&lt;span style="color: #FFAD66"&gt;from:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; message.&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;from&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt;, to&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; message.to, raw&lt;/span&gt;&lt;span style="color: #FFAD66"&gt;:&lt;/span&gt;&lt;span style="color: #d4d2c8"&gt; body})&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 webhook url and secret are environment variables, configured in the worker settings. Cloudflare handily exposes the results of their security checks in the &lt;code&gt;message.headers['authentication-results']&lt;/code&gt;, which makes verifying dkim and dmarc for the originating email easy &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;div class="admonition not-prose warning"&gt;
&lt;p class="admonition-title"&gt;Update (May 8)&lt;/p&gt;
&lt;p&gt;Cloudflare stopped sending the &lt;code&gt;authentication-results&lt;/code&gt; header, breaking this approach. Instead, parse the headers from the email.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Et voilà - free programmatic email access via webhooks in a few short steps.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;In my application, I chose to forward the headers to my backend to keep the worker code as simple as possible.&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>Good List Week 5</title><link href="https://blog.tmk.name/2026/05/04/good-list-week-5/" rel="alternate"></link><published>2026-05-04T00:00:00-04:00</published><updated>2026-05-04T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-05-04:/2026/05/04/good-list-week-5/</id><summary type="html">&lt;h2&gt;Contra dancing&lt;/h2&gt;
&lt;p&gt;&lt;img alt="contra" src="https://blog.tmk.name/images/contra.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Oh, what fun - wholesome smiling sweating moving dancing - I had at my first contra dance this weekend. I was invited by a friend, and because my girlfriend is a dancer, I suggested we go - nervous as I am, as someone with little dancing experience. I expected something stiff …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Contra dancing&lt;/h2&gt;
&lt;p&gt;&lt;img alt="contra" src="https://blog.tmk.name/images/contra.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Oh, what fun - wholesome smiling sweating moving dancing - I had at my first contra dance this weekend. I was invited by a friend, and because my girlfriend is a dancer, I suggested we go - nervous as I am, as someone with little dancing experience. I expected something stiff and ballroom like, something like I've seen in old English country dances in the movies.. all my expectations were cast aside, as I was thrown into the spinning and twirling.&lt;/p&gt;
&lt;p&gt;Contra is a social event, incredibly welcoming to newcomers. Dancers partner up each dance, asking or being asked, in a friendly way. Each dance is composed of a series of moves, e.g. do-si-do and balance and swing, that repeat in a fluid motion up and down the dance line, changing matching partners every set. The structure exposes the dancer to interact with just about everyone else at the event.&lt;/p&gt;
&lt;p&gt;For me, I really dig the structured dance. For one, it's a forcing function that exposed me to interact with just about everyone else, hands-on and face-to-face, quite the difference from the interpersonal distance I'm accustomed to, with strangers nonetheless. For another, the structure isn't total, with dancers improvising and playing, adding their own style to the mix. Finally the energy of a room of friendly people dancing to live music was just infectious; I didn't realize how exhausted I was until I walked out the door.&lt;/p&gt;
&lt;p&gt;I've never seen so many smiles on so many faces for something so wholesome.&lt;/p&gt;</content><category term="good list"></category></entry><entry><title>Good List Week 4</title><link href="https://blog.tmk.name/2026/04/27/good-list-week-4/" rel="alternate"></link><published>2026-04-27T00:00:00-04:00</published><updated>2026-04-27T00:00:00-04:00</updated><author><name>Tristan Kernan</name></author><id>tag:blog.tmk.name,2026-04-27:/2026/04/27/good-list-week-4/</id><summary type="html">&lt;h2&gt;Jersey Shore&lt;/h2&gt;
&lt;p&gt;Oh, the weather has just been unpredictable these past few weeks. From that brutal, brutal winter to a false summer of a few days, to now chilly, dreary spring. Well, I was fortunate enough to get to the beach on a warm day, and bask in the ocean …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Jersey Shore&lt;/h2&gt;
&lt;p&gt;Oh, the weather has just been unpredictable these past few weeks. From that brutal, brutal winter to a false summer of a few days, to now chilly, dreary spring. Well, I was fortunate enough to get to the beach on a warm day, and bask in the ocean.&lt;/p&gt;
&lt;p&gt;I reminisced - I went to the beach as a child, but my memories are so fragmented, I couldn't recall any one trip clearly. Even without the recollection, I felt the waves crash deep within me, and found some small peace to take home.&lt;/p&gt;
&lt;h2&gt;Improv&lt;/h2&gt;
&lt;p&gt;Montclair film, aka the Clairidge, puts on a few shows just about every month, including an improv show. This was my third time going, and it was nice, clean fun for an evening. I've been exhausted lately, watching way more television than I'd like, so the opportunity to watch live community theatre was hard to pass up on.&lt;/p&gt;
&lt;p&gt;It's also fun to engage, shout out suggestions for the performers. The audience volunteer this time was a hoot, an older lady who clammed up and refused to share any definitive details about her life: how many kids does she have? maybe 3, maybe 4; what was her career? better not to say! It was hilarious.&lt;/p&gt;
&lt;h2&gt;Card Games&lt;/h2&gt;
&lt;p&gt;Nothing like a deck of cards to bring people together. I'm fairly activity oriented (as opposed to lounge oriented), so the chance to play some card games this weekend with friends was welcome. I learned the game slap-jack, which is similar to spit, but purely focused on slapping the pile as fast as possible. I get so animated with games like this, it's a fun rush to win some hands.&lt;/p&gt;</content><category term="good list"></category></entry><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;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gfs/monet.jpg" class="glightbox" data-gallery="gallery-8" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gfs/monet.jpg" alt="Monet-inspired garden sculpture"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gfs/painter.jpg" class="glightbox" data-gallery="gallery-8" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gfs/painter.jpg" alt="Sculpture of a painter"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/gfs/peacock.jpg" class="glightbox" data-gallery="gallery-8" data-description=""&gt;&lt;img src="https://blog.tmk.name/images/gfs/peacock.jpg" alt="Peacock in the gardens"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&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="good list"></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="good list"></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="good list"></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;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/wikitrack/full.png" class="glightbox" data-gallery="gallery-5" data-description="Full network graph. Ring is pages not connected. The dense inner web demonstrates how highly connected wikipedia pages are"&gt;&lt;img src="https://blog.tmk.name/images/wikitrack/full.png" alt="Full network graph"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/wikitrack/scifi.png" class="glightbox" data-gallery="gallery-5" data-description="Zoom in on my favorite author: Iain Banks and the Culture series"&gt;&lt;img src="https://blog.tmk.name/images/wikitrack/scifi.png" alt="Sci-fi cluster"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/wikitrack/greece.png" class="glightbox" data-gallery="gallery-5" data-description="Cluster of ancient Greek philosophy and history"&gt;&lt;img src="https://blog.tmk.name/images/wikitrack/greece.png" alt="Ancient Greece cluster"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&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 …&lt;/p&gt;</summary><content type="html">&lt;div class="gallery not-prose"&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/wikitrack/full.png" class="glightbox" data-gallery="gallery-5" data-description="Full network graph. Ring is pages not connected. The dense inner web demonstrates how highly connected wikipedia pages are"&gt;&lt;img src="https://blog.tmk.name/images/wikitrack/full.png" alt="Full network graph"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/wikitrack/scifi.png" class="glightbox" data-gallery="gallery-5" data-description="Zoom in on my favorite author: Iain Banks and the Culture series"&gt;&lt;img src="https://blog.tmk.name/images/wikitrack/scifi.png" alt="Sci-fi cluster"&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="gallery-item"&gt;&lt;a href="https://blog.tmk.name/images/wikitrack/greece.png" class="glightbox" data-gallery="gallery-5" data-description="Cluster of ancient Greek philosophy and history"&gt;&lt;img src="https://blog.tmk.name/images/wikitrack/greece.png" alt="Ancient Greece cluster"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/div&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></feed>