<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jimmy Yeung</title>
    <description>The latest articles on DEV Community by Jimmy Yeung (@jimmyyeung).</description>
    <link>https://dev.to/jimmyyeung</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F698881%2F435cddf8-cec3-4dd5-bbb0-070b345b5013.png</url>
      <title>DEV Community: Jimmy Yeung</title>
      <link>https://dev.to/jimmyyeung</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jimmyyeung"/>
    <language>en</language>
    <item>
      <title>Journey of Systematically Cut Our Monorepo CI Time in Half</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Thu, 26 Mar 2026 21:51:34 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/journey-of-systematically-cut-our-monorepo-ci-time-in-half-ec8</link>
      <guid>https://dev.to/jimmyyeung/journey-of-systematically-cut-our-monorepo-ci-time-in-half-ec8</guid>
      <description>&lt;p&gt;The engineering team maintains a Python/TypeScript monorepo with 22+ Django microservices, a React frontend, and shared libraries — all running through GitHub Actions CI. Over time, CI had become the productivity bottleneck. PRs routinely waited 10-15 minutes for green checks, and the slowest pipeline consistently exceeded 10 minutes. Engineers were context-switching while waiting for builds, or worse, stacking PRs on top of unverified ones.&lt;/p&gt;

&lt;p&gt;I ran a focused initiative to systematically identify and eliminate CI bottlenecks. This post is a record of how I approached it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Finding the Bottleneck
&lt;/h2&gt;

&lt;p&gt;You can't optimize what you can't see. So my first instinct was to write a script to pull GitHub Actions workflow run data from the API and compute aggregate statistics — average, P50, P75, P90 — for both wall clock time and per-job durations across any date range.&lt;/p&gt;

&lt;p&gt;This immediately told me two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The largest service's unit-test job was the critical path&lt;/strong&gt; — at P50 of ~10 minutes, it single-handedly determined the CI wall clock time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other services stayed around 8-9 minutes&lt;/strong&gt; — they were running multiple jobs in parallel so further investigation was needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Having this data let me prioritize ruthlessly. Instead of optimizing everything at once, I focused on the jobs that actually moved the wall clock needle.&lt;/p&gt;

&lt;p&gt;And it's always satisfying to compare the before vs. after statistics.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Low-Hanging Fruit — Modernizing the Toolchain
&lt;/h2&gt;

&lt;p&gt;My first lever was swapping slow tools for faster alternatives. The Rust-based ecosystem has matured to the point where several tools are genuine drop-in replacements, so this felt like a no-brainer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Yarn to Bun (not Rust-written, but famous for its speed and maturity)&lt;/li&gt;
&lt;li&gt;Webpack to Rspack&lt;/li&gt;
&lt;li&gt;storybook-rsbuild&lt;/li&gt;
&lt;li&gt;Adopt &lt;code&gt;eslint-plugin-oxlint&lt;/code&gt;, disabling ESLint rules that oxlint now handles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each swap reduced CI time per job by 30-40%.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Low-Hanging Fruit — Docker Build Optimization
&lt;/h2&gt;

&lt;p&gt;Docker builds were another time sink. I attacked from multiple angles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enabled &lt;code&gt;.dockerignore&lt;/code&gt;&lt;/strong&gt; — builds weren't using ignore files because the build context was outside their scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized &lt;code&gt;uv sync&lt;/code&gt; in Dockerfiles&lt;/strong&gt; — eliminated unnecessary dependency installation loops; &lt;code&gt;uv build --package&lt;/code&gt; uses build isolation and never touches the workspace venv&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaned up stale cross-service dependencies&lt;/strong&gt; — after migrating to uv workspaces, some services had phantom dependencies on unrelated services, triggering unnecessary rebuilds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduced Docker build CI time by about 50%.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Test Duration Enforcement &amp;amp; Slow Test Fixes
&lt;/h2&gt;

&lt;p&gt;After tackling the low-hanging fruit, I turned to the tests themselves. The repo already had a &lt;code&gt;check-test-durations&lt;/code&gt; composite action that parses JUnit XML and reports the top 10 slowest tests — but it was only wired up in 2 of 22 services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rolling Out Visibility
&lt;/h3&gt;

&lt;p&gt;I added the duration check to all workspace CI workflows with &lt;code&gt;continue-on-error: true&lt;/code&gt; — visibility without blocking builds.&lt;/p&gt;

&lt;p&gt;This immediately surfaced tests taking 10-30+ seconds across services that had never been profiled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing the Worst Offenders
&lt;/h3&gt;

&lt;p&gt;With data in hand, I targeted the slowest individual tests. Some were making unnecessary network calls, others were creating excessive test data, and a few were simply doing too much in a single test. I fixed tests exceeding 10 seconds across the two slowest services, bringing meaningful improvements before touching any CI infrastructure.&lt;/p&gt;

&lt;p&gt;(One fun find: somewhere the application code called &lt;code&gt;time.sleep(10)&lt;/code&gt;, and the test didn't mock it — so the test took at least 10 seconds for free. Sometimes reducing test duration is that simple.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforcing the Threshold
&lt;/h3&gt;

&lt;p&gt;After the initial round of fixes, I made the duration check a hard enforcement — any individual test exceeding 10 seconds fails the build. This prevents slow tests from creeping back in, turning a one-time fix into a sustainable practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Caching: Every Layer Counts
&lt;/h2&gt;

&lt;p&gt;GitHub Actions provides a 10 GB cache quota per repository. In a monorepo with 22+ services, that's not much — so every cache needs to earn its space. I identified and enabled caching at four layers of the CI pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.git&lt;/code&gt; Cache
&lt;/h3&gt;

&lt;p&gt;In a monorepo, &lt;code&gt;git clone&lt;/code&gt; with &lt;code&gt;fetch-depth: 0&lt;/code&gt; (needed for change detection and visual regression diff baselines) is expensive — the repo has significant history. I set up a scheduled workflow that runs every 6 hours to pre-populate the &lt;code&gt;.git&lt;/code&gt; directory cache on master.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;uv&lt;/code&gt; Cache
&lt;/h3&gt;

&lt;p&gt;I enabled &lt;code&gt;setup-uv&lt;/code&gt;'s built-in cache across all CI workflow files.&lt;/p&gt;

&lt;p&gt;The cache is keyed on the &lt;code&gt;uv.lock&lt;/code&gt; hash, so it invalidates exactly when dependencies change. Impact: ~5 seconds saved per job on the "Install uv" step (8s → 3s average). Small per-job, just a few hundred KB.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;node_modules&lt;/code&gt; Cache
&lt;/h3&gt;

&lt;p&gt;The frontend workflows cache &lt;code&gt;node_modules&lt;/code&gt; after &lt;code&gt;bun install&lt;/code&gt;. The cache key was previously overloaded — some callers passed a branch name (&lt;code&gt;${{ github.head_ref }}&lt;/code&gt;), others a static date string. Branch-keyed caches created redundant entries for the same lockfile, wasting quota.&lt;/p&gt;

&lt;p&gt;I simplified to a single lockfile-based key using &lt;code&gt;bun.lock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GitHub Actions already scopes cache access by branch (PRs can read from the base branch's cache but not other PRs'), so per-branch keys were pure overhead. This change eliminated redundant cache entries and improved hit rates.&lt;/p&gt;

&lt;h3&gt;
  
  
  ESLint Cache
&lt;/h3&gt;

&lt;p&gt;ESLint is one of the slower steps in frontend validation. I enabled its persistent cache by restoring &lt;code&gt;.eslintcache&lt;/code&gt; between runs.&lt;/p&gt;

&lt;p&gt;On cache hit, ESLint only re-lints files that changed since the last run, skipping the majority of the codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Test Parallelization (with Cost in Mind)
&lt;/h2&gt;

&lt;p&gt;Parallelization is the most effective lever for reducing wall clock time — but in GitHub Actions, more parallel jobs means more billable minutes. Every matrix shard spins up a fresh runner, installs dependencies, and tears down. The setup overhead isn't free. I approached this deliberately, targeting parallelization where the payoff justified the cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  pytest-xdist: Free Parallelism
&lt;/h3&gt;

&lt;p&gt;The lowest-cost optimization is utilizing all CPUs within a single runner. I enabled &lt;code&gt;pytest-xdist&lt;/code&gt; with &lt;code&gt;-n auto&lt;/code&gt; in services that were running tests serially.&lt;/p&gt;

&lt;p&gt;For one service, this cut pytest execution from ~4m30s to ~2m07s — roughly 2x faster with zero additional runner cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Matrix Sharding: Deliberate Trade-off
&lt;/h3&gt;

&lt;p&gt;For the largest service (8,674 tests, ~10 minute P50), single-runner parallelism wasn't enough. I restructured its CI into 7 matrix shards to keep each under 5 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost trade-off&lt;/strong&gt;: 7 shards means 7x the setup overhead (dependency installation, database creation). Total billable minutes actually increased. But the wall clock time — what engineers wait on — dropped from ~10 minutes to ~5 minutes. For a team making dozens of PRs daily, the productivity gain far outweighs the compute cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure first, always.&lt;/strong&gt; A simple Python script using the &lt;code&gt;gh&lt;/code&gt; CLI gave me reproducible P50/P75/P90 data across any date range. Way more reliable than eyeballing the Actions UI — and it makes before/after comparisons trivial.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modernize your toolchain — it's free speed.&lt;/strong&gt; Bun, Rspack, and oxlint are drop-in replacements that delivered 30-40% speedups per job with minimal migration effort. Highest ROI work I did.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instrument test durations, then enforce them.&lt;/strong&gt; A 30-second test hiding in a 6-minute suite is invisible until you look. These are often the easiest wins — and a hard time budget prevents regression.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In a monorepo, cache quota is a shared resource.&lt;/strong&gt; GitHub Actions' 10 GB limit is shared across all workflows. A poorly-keyed cache doesn't just waste space — it actively evicts caches that matter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parallelism costs money — spend it wisely.&lt;/strong&gt; pytest-xdist within a single runner is free performance. Matrix sharding trades billable minutes for wall clock time. Make that trade deliberately, and know the numbers before and after.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ci</category>
      <category>monorepo</category>
      <category>devvelocity</category>
      <category>devex</category>
    </item>
    <item>
      <title>Journey migrating to uv workspace</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Mon, 02 Feb 2026 03:21:33 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/journey-migrating-to-uv-workspace-34a7</link>
      <guid>https://dev.to/jimmyyeung/journey-migrating-to-uv-workspace-34a7</guid>
      <description>&lt;p&gt;My company has migrated from poly-repository to a single monorepo recently due to various reasons, like &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we always want our internal packages &lt;strong&gt;up-to-date&lt;/strong&gt; and &lt;strong&gt;in-sync&lt;/strong&gt; of each other -&amp;gt; there's little gain in maintaining versions for our internal packages (not the main point of the blog today). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And for dependency management, we are still using the poly-repo way of&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each repo (now dir in monorepo) has its own &lt;code&gt;requirements.txt&lt;/code&gt; and its own virtual environment. We will spin each virtual environment up during local development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And of course...the pain points were real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slow setup&lt;/strong&gt;: Full environment setup took 12+ minutes, with libraries being redundantly rebuilt for each service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version drift&lt;/strong&gt;: Service A might run Django 4.2 while Service B ran Django 5.1, leading to subtle bugs when code was shared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; introduced workspaces, we saw an opportunity to modernize. The goal: a single &lt;code&gt;uv.lock&lt;/code&gt; file guaranteeing consistent dependencies across all services, with faster installation via shared caching.&lt;/p&gt;

&lt;p&gt;This is the story of that migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Aligning Dependencies (The Hidden Prerequisite)
&lt;/h2&gt;

&lt;p&gt;Before we could create a unified workspace, we faced an uncomfortable truth: &lt;strong&gt;UV workspaces enforce a single version per package across all services&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Our per-service approach had been hiding version conflicts for years. When we analyzed our codebase, we found:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Service A&lt;/th&gt;
&lt;th&gt;Service B&lt;/th&gt;
&lt;th&gt;Service C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Django&lt;/td&gt;
&lt;td&gt;4.2.x&lt;/td&gt;
&lt;td&gt;5.1.x&lt;/td&gt;
&lt;td&gt;4.2.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;factory_boy&lt;/td&gt;
&lt;td&gt;3.2.x&lt;/td&gt;
&lt;td&gt;3.3.x&lt;/td&gt;
&lt;td&gt;3.2.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pytest&lt;/td&gt;
&lt;td&gt;7.4.x&lt;/td&gt;
&lt;td&gt;8.0.x&lt;/td&gt;
&lt;td&gt;7.4.x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With pip's per-venv approach, this "worked" because each service lived in isolation. With UV workspaces, we needed one truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The migration began before the migration&lt;/strong&gt;: we spent time upgrading packages across all services to consistent versions, fixing breaking changes from major version bumps along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key learning&lt;/strong&gt;: UV workspaces expose hidden version conflicts that pip's per-venv approach masks. Resolve these conflicts &lt;em&gt;before&lt;/em&gt; migration, not during.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2: Migrating Libraries First
&lt;/h2&gt;

&lt;p&gt;With dependencies aligned, we started with the foundation: our shared libraries. This was strategically important because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Libraries have fewer dependencies than services&lt;/li&gt;
&lt;li&gt;All services depend on libraries, so migrating them first tests the workspace machinery&lt;/li&gt;
&lt;li&gt;Subsequent service migrations become simpler&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Workspace Structure
&lt;/h3&gt;

&lt;p&gt;We created a virtual workspace root at &lt;code&gt;your_workspace/pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-workspace"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0"&lt;/span&gt;
&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Your Monorepo Workspace - Virtual root package"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="nn"&gt;[tool.uv]&lt;/span&gt;
&lt;span class="py"&gt;package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c"&gt;# Virtual workspace - don't install this package&lt;/span&gt;
&lt;span class="py"&gt;index-strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"unsafe-best-match"&lt;/span&gt;

&lt;span class="nn"&gt;[tool.uv.workspace]&lt;/span&gt;
&lt;span class="py"&gt;members&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"internal-package-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"internal-package-b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"internal-package-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c"&gt;# ... more packages&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;exclude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"not-yet-migrated"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="py"&gt;internal-package-a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;internal-package-b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;internal-package-c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;# All workspace packages declared as sources&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the library migration, local setup time improved from 12 minutes to 9 minutes, since libraries were now built once and cached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learning: The Dependency Bloat Discovery
&lt;/h3&gt;

&lt;p&gt;When we migrated our first service, we hit an unexpected problem. setting up in dependent services started failing with installation errors for packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The investigation revealed a fundamental difference:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;How it installs internal-package-a&lt;/th&gt;
&lt;th&gt;Dependencies installed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pip (old)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-e file://../internal-package-a#egg=internal-package-a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;None&lt;/strong&gt; - only adds source to &lt;code&gt;sys.path&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UV workspace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;uv sync&lt;/code&gt; with PEP 621&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;All&lt;/strong&gt; - follows the spec correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The old pip approach was a &lt;em&gt;hack&lt;/em&gt;. It only added the package source to the Python path without installing any of its dependencies. This "worked" because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dependent services only imported specific utility modules &lt;/li&gt;
&lt;li&gt;Common deps like Django were redundantly declared in each service's &lt;code&gt;requirements.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;UV was doing the &lt;em&gt;correct&lt;/em&gt; thing by installing all declared dependencies.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Solution: Optional Dependencies
&lt;/h4&gt;

&lt;p&gt;The problem was that &lt;code&gt;internal-package-a&lt;/code&gt; conflated two concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The running service&lt;/strong&gt;: Needs a, b, c, x, y, z&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The exported libraries&lt;/strong&gt;: Only need a, b, c&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We split them using optional dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c"&gt;# Minimal deps for exported libraries (what consumers actually need)&lt;/span&gt;
    &lt;span class="s"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[project.optional-dependencies]&lt;/span&gt;
&lt;span class="py"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c"&gt;# Runtime deps only needed when running the actual service&lt;/span&gt;
    &lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consumers get minimal dependencies by default&lt;/li&gt;
&lt;li&gt;Running the service locally: &lt;code&gt;uv sync --extra service&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Production Dockerfiles: &lt;code&gt;uv export --extra service&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Phase 3: Service-by-Service Migration
&lt;/h2&gt;

&lt;p&gt;With the pattern established, we migrated services in dependency order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First&lt;/strong&gt;: Services that only depend on libraries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then&lt;/strong&gt;: Services with inter-service dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finally&lt;/strong&gt;: Services with the most dependents&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each service migration followed the same checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move &lt;code&gt;requirements.txt&lt;/code&gt; contents to &lt;code&gt;[project].dependencies&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Separate service-only deps into &lt;code&gt;[project.optional-dependencies].service&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add to workspace &lt;code&gt;members&lt;/code&gt; list&lt;/li&gt;
&lt;li&gt;Update Makefile to use &lt;code&gt;uv sync --package &amp;lt;name&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Update Dockerfile (this is where things got interesting...)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Learning: The Docker Editable Install Trap
&lt;/h3&gt;

&lt;p&gt;This was our most painful discovery. After deploying a migrated service to QA, we saw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ModuleNotFoundError: No module named 'configurations'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mystery: &lt;code&gt;django-configurations&lt;/code&gt; was definitely installed during the Docker build. Even stranger, &lt;strong&gt;the same image behaved differently&lt;/strong&gt; depending on the pod:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DB job pods: Worked fine&lt;/li&gt;
&lt;li&gt;Runner/worker pods: Failed with import errors&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Root Cause
&lt;/h4&gt;

&lt;p&gt;When &lt;code&gt;uv export&lt;/code&gt; generates requirements, local workspace packages appear as paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./internal-package-a
./internal-package-b
./internal-package-c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;uv pip install&lt;/code&gt; sees a local path (even without &lt;code&gt;-e&lt;/code&gt;), it creates an &lt;strong&gt;editable-style install&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;__editable__.internal_package_a-0.0.0.pth
__editable___internal_package_a_0_0_0_finder.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These &lt;code&gt;.pth&lt;/code&gt; files add path hooks that reference the source directories (&lt;code&gt;/internal-package-a&lt;/code&gt;). In our multi-stage Docker build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build stage: Source directories exist (we &lt;code&gt;COPY . .&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Final stage: Only &lt;code&gt;/usr/local/lib&lt;/code&gt; is copied, source dirs don't exist&lt;/li&gt;
&lt;li&gt;Python startup: &lt;code&gt;.pth&lt;/code&gt; files try to hook non-existent paths, causing unpredictable import failures&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Why &lt;code&gt;--no-editable&lt;/code&gt; Doesn't Help
&lt;/h4&gt;

&lt;p&gt;We tried &lt;code&gt;uv export --no-editable&lt;/code&gt;, but it only affects the &lt;em&gt;output format&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# With --no-editable:&lt;/span&gt;
./internal-package-a

&lt;span class="c"&gt;# Without --no-editable:&lt;/span&gt;
&lt;span class="nt"&gt;-e&lt;/span&gt; ./internal-package-a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are still local paths. &lt;code&gt;uv pip install&lt;/code&gt; treats them identically.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Fix: Build wheels for local packages first
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--package&lt;/span&gt; your-service &lt;span class="nt"&gt;--extra&lt;/span&gt; service &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt; &lt;span class="nt"&gt;--no-emit-package&lt;/span&gt; your-service &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /requirements-raw.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="c"&gt;# Extract local packages (lines starting with ./)&lt;/span&gt;
    grep '^\./' /requirements-raw.txt &amp;gt; /local-packages.txt &amp;amp;&amp;amp; \
    # Filter out local paths for external deps
    grep -v '^\./' /requirements-raw.txt | grep -v '^[[:space:]]*#' &amp;gt; /requirements.txt &amp;amp;&amp;amp; \
    # Build wheels for each local package
    mkdir -p /wheels &amp;amp;&amp;amp; \
    while read -r pkg; do \
        uv build --wheel --out-dir /wheels "$pkg"; \
    done &amp;lt; /local-packages.txt &amp;amp;&amp;amp; \
    # Install external dependencies
    uv pip install --system -r /requirements.txt &amp;amp;&amp;amp; \
    # Install local packages from wheels (NOT source paths)
    uv pip install --system /wheels/*.whl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wheels are self-contained. When installed, they extract directly to site-packages with no references to source directories.&lt;/p&gt;

&lt;h4&gt;
  
  
  Verification
&lt;/h4&gt;

&lt;p&gt;We added a check to catch regressions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /usr/local/lib/python3.12/site-packages/__editable__&lt;span class="k"&gt;*&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: Editable installs found - these will fail at runtime!"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK: No editable installs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;After migrating all libraries and services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;make setup&lt;/code&gt; time&lt;/td&gt;
&lt;td&gt;~12 min&lt;/td&gt;
&lt;td&gt;~1 min (a lot faster!!!)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual environments&lt;/td&gt;
&lt;td&gt;One per service&lt;/td&gt;
&lt;td&gt;Single workspace venv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency consistency&lt;/td&gt;
&lt;td&gt;Hoped for&lt;/td&gt;
&lt;td&gt;Guaranteed by &lt;code&gt;uv.lock&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config files&lt;/td&gt;
&lt;td&gt;Multiple &lt;code&gt;requirements.txt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Single &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Recommendations for Your Migration
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with a spike&lt;/strong&gt;: Migrate one service end-to-end to understand the full scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invest in dependency alignment upfront&lt;/strong&gt;: This is the hidden work that makes everything else possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for Dockerfile changes&lt;/strong&gt;: The wheel-building pattern should be templated across services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communicate timing&lt;/strong&gt;: Multi-branch repos need coordination to avoid merge hell&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Migrating to UV workspaces was a significant undertaking, touching every service and library in our monorepo. The upfront investment in dependency alignment and the debugging of Docker editable install issues were the hardest parts.&lt;/p&gt;

&lt;p&gt;But the result is worth it: a single source of truth for dependencies, faster local development.&lt;/p&gt;

&lt;p&gt;If you're managing a Python monorepo and suffering from version drift or slow setup times, UV workspaces are worth the investment. Just watch out for those editable installs.&lt;/p&gt;

</description>
      <category>python</category>
      <category>uv</category>
      <category>monorepo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Django leaky connection pool</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Thu, 07 Aug 2025 20:49:47 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/django-leaky-connection-pool-2chj</link>
      <guid>https://dev.to/jimmyyeung/django-leaky-connection-pool-2chj</guid>
      <description>&lt;h3&gt;
  
  
  Disclaimer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In the case described below, we are &lt;strong&gt;NOT using traditional Django web server&lt;/strong&gt;. We are just leverage django as a framework and start our own grpc server. That's why we hit this tricky scenario, that's used to be handled by django automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Background
&lt;/h3&gt;

&lt;p&gt;Django web server will close old connection when web request starts / finishes using signals: &lt;a href="https://github.com/django/django/blob/5.2.5/django/db/__init__.py#L62-L63" rel="noopener noreferrer"&gt;https://github.com/django/django/blob/5.2.5/django/db/__init__.py#L62-L63&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Register an event to reset transaction state and close connections past
# their lifetime.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;close_old_connections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initialized_only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close_if_unusable_or_obsolete&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_started&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close_old_connections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_finished&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close_old_connections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yet, &lt;code&gt;request_started&lt;/code&gt; and &lt;code&gt;request_finished&lt;/code&gt; will only be fired &lt;strong&gt;upon web requests&lt;/strong&gt;. If we're not using django for web server, we need to bite the bullet and handle close connection on our own.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we did
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Interceptor on request
&lt;/h4&gt;

&lt;p&gt;We have a &lt;code&gt;grpc.ServerInterceptor&lt;/code&gt;, which closes the connection before request and after response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerInterceptor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;before_request&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt;
        &lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close_old_connections&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;after_response&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt;
        &lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close_old_connections&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;interceptors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;DatabaseInterceptor&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Define an internal ThreadPoolExecutor that helps us to close connection
&lt;/h4&gt;

&lt;p&gt;We need this because the new threads spawned will get a connection if it runs a database query -&amp;gt; but will never return the connection back to the pool&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close_old_connections&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other alternatives but not considered
&lt;/h3&gt;

&lt;p&gt;We considered the &lt;code&gt;on_commit&lt;/code&gt; &lt;a href="https://docs.djangoproject.com/en/5.2/topics/db/transactions/#performing-actions-after-commit" rel="noopener noreferrer"&gt;callback&lt;/a&gt; like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close_old_connections&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;such that developers don't need to always remember to use the predefined &lt;code&gt;DatabaseThreadPoolExecutor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However &lt;a href="https://docs.djangoproject.com/en/5.2/topics/db/transactions/#why-no-rollback-hook" rel="noopener noreferrer"&gt;Django doesn't provide rollback hook&lt;/a&gt;, that makes it not a good solution because we do want to release connection whenever there's an exception in an atomic block.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thoughts
&lt;/h3&gt;

&lt;p&gt;This is very tricky to discover but we cannot blame django for this. Django is built for handling web requests, it's on us using the framework to do what it's not supposed to do.&lt;/p&gt;

&lt;p&gt;Yet, hope this helps anyone who face this situation and provide some insights. :D&lt;/p&gt;

</description>
      <category>django</category>
      <category>postgres</category>
      <category>database</category>
      <category>connectionpool</category>
    </item>
    <item>
      <title>Upgrade from psycopg2 to psycopg3 in Django</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Sat, 10 May 2025 20:42:17 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/upgrade-to-django-5-with-psycopg3-4e8b</link>
      <guid>https://dev.to/jimmyyeung/upgrade-to-django-5-with-psycopg3-4e8b</guid>
      <description>&lt;h2&gt;
  
  
  Why Psycopg3
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. No upcoming features for psycopg2
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Psycopg2 is &lt;strong&gt;not&lt;/strong&gt; expected to receive new features anymore

&lt;ul&gt;
&lt;li&gt;From &lt;a href="https://pypi.org/project/psycopg2/" rel="noopener noreferrer"&gt;psycopg2 Pypi description&lt;/a&gt;:&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The psycopg2 package is still widely used and actively maintained, but it is &lt;strong&gt;not expected&lt;/strong&gt; to receive new features.&lt;/p&gt;

&lt;p&gt;Psycopg 3 is the evolution of psycopg2 and is where new features are being developed: if you are starting a new project you should probably start from 3!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. More modern features in psycopg3
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt; Psycopg3, being the successor of psycopg3, presents a familiar interface for everyone who has used Psycopg 2 or any other DB-API 2.0 database adapter, but allows to use &lt;strong&gt;more modern PostgreSQL and Python features&lt;/strong&gt;.(&lt;a href="https://www.psycopg.org/psycopg3/docs/#psycopg-3-postgresql-database-adapter-for-python" rel="noopener noreferrer"&gt;cite&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. More community support psycopg3 like Django
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Django starts to &lt;a href="https://django.readthedocs.io/en/latest/releases/4.2.html#psycopg-3-support" rel="noopener noreferrer"&gt;support psycopg3 in Django 4.2&lt;/a&gt;. And &lt;a href="https://docs.djangoproject.com/en/5.1/ref/databases/#connection-pool" rel="noopener noreferrer"&gt;Django 5.1 also introduces connection pooling with psycopg3&lt;/a&gt;, that gives us the incentive to upgrade both Django &amp;amp; psycopg3, so as to unleash us from various features going to be introduced in psycopg3. First being the connection pooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to upgrade
&lt;/h2&gt;

&lt;p&gt;psycopg3 already provided the differences to psycopg2 (&lt;a href="https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html" rel="noopener noreferrer"&gt;Ref&lt;/a&gt;). What I'm trying to illustrate below are the gotchas when I tried to update my repository using Django to it, &lt;strong&gt;which is not mentioned in the aforementioned link&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. psycopg2 and psycopg3 cannot co-exist
&lt;/h3&gt;

&lt;p&gt;I tried &lt;code&gt;pip install psycopg&lt;/code&gt; and my unit tests immediately fail, possibly due to some mixed imports.&lt;/p&gt;

&lt;p&gt;We could take reference to django as to &lt;a href="https://github.com/django/django/blob/ea1e3703bee28bfbe4f32ceb39ad31763353b143/django/db/backends/postgresql/psycopg_any.py#L72" rel="noopener noreferrer"&gt;how they handle the case&lt;/a&gt; by &lt;code&gt;try ... except ImportError ...&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Test coverage
&lt;/h3&gt;

&lt;p&gt;We have 2 imports but they can't co-exist, so how should our tests run?&lt;/p&gt;

&lt;p&gt;We were using &lt;code&gt;coverage run&lt;/code&gt; previously. And now as we're maintaining both packages usages, we will need to make use of &lt;code&gt;coverage combine&lt;/code&gt;. I.e.&lt;/p&gt;

&lt;p&gt;From:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;coverage run &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TEST_COMMAND&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
coverage report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# temp workaround becoz psycopg2 and psycopg have mixed imports.&lt;/span&gt;
&lt;span class="c"&gt;# So we need to temp. uninstall psycopg and run the original tests in psycopg2.&lt;/span&gt;
&lt;span class="c"&gt;# Will fallback to install deps again and raise error so it doesn't disrupt the dev experience.&lt;/span&gt;
pip uninstall &lt;span class="nt"&gt;-y&lt;/span&gt; psycopg &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; coverage run &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.coverage.psycopg2 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TEST_COMMAND&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEPENDENCY_INSTALLATION_COMMAND&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c"&gt;# Install again for psycopg3 tests&lt;/span&gt;
&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEPENDENCY_INSTALLATION_COMMAND&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
coverage run &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.coverage.psycopg3 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TEST_COMMAND&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Finally, combine coverage &amp;amp; generate report&lt;/span&gt;
coverage combine &lt;span class="nt"&gt;--append&lt;/span&gt; &lt;span class="nt"&gt;--keep&lt;/span&gt;  &lt;span class="nt"&gt;--debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pathmap
coverage report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. DateTimeTZRange changed to TimestamptzRange
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Should be fairly easy and no need to explain)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg2.extras&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DateTimeTZRange&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg.types.range&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TimestamptzRange&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Set CONN_MAX_AGE = 0
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CONN_MAX_AGE&lt;/code&gt; is a Django setting that controls the lifetime of connections managed directly by Django. It dictates how long Django keeps a connection open before closing and reopening it for subsequent requests.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CONN_MAX_AGE = 0 (default)&lt;/code&gt;: Connection is closed at the end of each request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CONN_MAX_AGE &amp;gt; 0&lt;/code&gt;: Connection persists for that many seconds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CONN_MAX_AGE = None&lt;/code&gt;: Connection persists indefinitely (until unusable).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When using psycopg3's native pooling via &lt;code&gt;OPTIONS['pool']&lt;/code&gt;, the &lt;code&gt;psycopg_pool&lt;/code&gt; library takes over the lifecycle management of connections within its pool. Django requests connections from this pool. To avoid conflicting connection management behaviors, it is generally recommended to set &lt;code&gt;CONN_MAX_AGE = 0&lt;/code&gt; when &lt;code&gt;psycopg_pool&lt;/code&gt; is active. This ensures Django doesn't prematurely close connections that the pool intends to keep alive and manage. The pool's &lt;code&gt;max_lifetime&lt;/code&gt; and &lt;code&gt;max_idle&lt;/code&gt; parameters should govern connection longevity.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Django adapter
&lt;/h3&gt;

&lt;p&gt;Psycopg3 has the adapters documented &lt;a href="https://www.psycopg.org/psycopg3/docs/basic/adapt.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;, but not in the aforementioned "difference page".&lt;/p&gt;

&lt;p&gt;An example I faced is with JSON adaptation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg2.extras&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO mytable VALUES (%s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now in psycopg3, we need to do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg.types.json&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Jsonb&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO mytable VALUES (%s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to enable connection pool
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://docs.djangoproject.com/en/5.2/ref/databases/#connection-pool" rel="noopener noreferrer"&gt;Django documentation&lt;/a&gt;, it already stated that we could set "pool" in the &lt;code&gt;OPTIONS&lt;/code&gt; part of the database configuration, just stating here for visibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
# parameters like max_size could be found in https://www.psycopg.org/psycopg3/docs/api/pool.html
&lt;/span&gt;
&lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPTIONS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;your_desierd_number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Overall it's fairly easy to upgrade from psycopg2 to psycopg3 for my Django project, I guess most of the heavy liftings have been done by Django already. Hopefully that helps solve the problems others are facing too!&lt;/p&gt;

</description>
      <category>psycopg3</category>
      <category>django</category>
      <category>connectionpool</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Confident feature release - use dry-run</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Sun, 16 Jul 2023 07:21:20 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/confident-feature-release-use-dry-run-2737</link>
      <guid>https://dev.to/jimmyyeung/confident-feature-release-use-dry-run-2737</guid>
      <description>&lt;h3&gt;
  
  
  Background
&lt;/h3&gt;

&lt;p&gt;We have feature releases every day - and in every case, we would love to verify if the business logic of that feature is correct or not. &lt;/p&gt;

&lt;p&gt;One normal approach is to &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;have a feature flag, all codes are hidden behind the feature flag and we only switch to the new flow when the feature flag is on. &lt;/li&gt;
&lt;li&gt;Do unit tests, integration tests, stress tests for possible scenarios&lt;/li&gt;
&lt;li&gt;Have the QA team to carry out end-to-end or even further exploratory tests before flipping the feature flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It looks promising. But sometimes the flow is so complicated that we may not cover all possible scenarios in our test suites. How could we be more confident?&lt;/p&gt;

&lt;h3&gt;
  
  
  Dry Run Mode
&lt;/h3&gt;

&lt;p&gt;In one project (e.g. a web application using Django, React, Postgres), we were doing a large refactoring of an existing feature. We introduced the dry run mechanism so that we can &lt;strong&gt;simultaneously run the old endpoint and new endpoint; but the new endpoint is only in dry run mode&lt;/strong&gt;. In this particular case, it could mean creating new db columns with prefix &lt;code&gt;dry_run&lt;/code&gt; - and we are only writing to the &lt;code&gt;dry_run_*&lt;/code&gt; columns in dry run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Behaviour
&lt;/h3&gt;

&lt;p&gt;With feature flag OFF&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use old endpoint as source-of-truth&lt;/li&gt;
&lt;li&gt;but call the new endpoint with a new boolean request parameter called &lt;code&gt;dry_run&lt;/code&gt;. With &lt;code&gt;dry_run&lt;/code&gt; is True, we just update the new &lt;code&gt;dry_run_*&lt;/code&gt; columns.&lt;/li&gt;
&lt;li&gt;We could report any discrepancy if it turns out the &lt;code&gt;dry_run_*&lt;/code&gt; is NOT the same as the original ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With feature flag’s OFF, we are observing no error + data (&lt;code&gt;dry_run_*&lt;/code&gt;) is correct + QA passes; we could toggle the feature flag with confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;p&gt;The dry run mechanism indeed helps us to detect the missing logics with real user behaviours. It’s useful since it’s always hard to mimic all user behaviours in QA environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some Gotchas
&lt;/h3&gt;

&lt;p&gt;Although it brings some benefits, we also need to be cautious to avoid affecting production results. Here are some thinking directions and examples:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Avoid saving stale data
&lt;/h4&gt;

&lt;p&gt;Usually we make use of &lt;code&gt;.save()&lt;/code&gt; in Django for updating. It will generate a query like &lt;code&gt;UPDATE … SET &amp;lt;ALL_FIELDS&amp;gt; … WHERE id = …&lt;/code&gt; to update all the columns. For some critical queries, we have applied pessimistic lock with &lt;code&gt;select_for_update&lt;/code&gt; so that the rows returned by SELECT query are locked until the entire transaction is committed.&lt;br&gt;
However, &lt;strong&gt;not all updating queries apply pessimistic lock&lt;/strong&gt;. Reasons could be &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there’s only one endpoint updating the table - we make use of this nature, so that we don’t apply the locking to have some performance gain. &lt;/li&gt;
&lt;li&gt;They rarely hit concurrency issues so it’s not worthwhile to introduce locking&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And in the parallel run world, we’re writing to the &lt;strong&gt;same&lt;/strong&gt; rows and thus saving stale data is a big concern. It also might not be worthwhile to introduce locking to the original flow because it slows down the performance.&lt;/p&gt;

&lt;p&gt;How we’re trying to solve this problem&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We made use of &lt;code&gt;update_fields&lt;/code&gt; argument in &lt;code&gt;.save()&lt;/code&gt; to specify which fields to update. This way we don’t need to apply locking. But we should remove them as part of the feature flag cleanup process to avoid confusion.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Old endpoint: 
&lt;/span&gt;
&lt;span class="n"&gt;update_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SomeTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_fields&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dry_run_column_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dry_run_column_2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Boolean flag that indicates if the field has a database column associated with it
&lt;/span&gt;    &lt;span class="c1"&gt;# Source:
&lt;/span&gt;    &lt;span class="c1"&gt;# https://docs.djangoproject.com/en/4.2/ref/models/fields/#django.db.models.Field.concrete
&lt;/span&gt;    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concrete&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# New endpoint
&lt;/span&gt;
&lt;span class="n"&gt;update_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"date_modified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dry_run_column_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dry_run_column_2"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  2. Keep performance in mind
&lt;/h4&gt;

&lt;p&gt;Since we’re simultaneously running 2 flows, &lt;strong&gt;extra overhead&lt;/strong&gt; shall be introduced. We should always keep performance in mind and incur as less overhead as possible &lt;/p&gt;

&lt;p&gt;How we’re trying to solve this problem&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We make use of &lt;code&gt;void&lt;/code&gt; instead of &lt;code&gt;await&lt;/code&gt; in React frontend very often when we try to call the new endpoint in the frontend. In this way, the frontend doesn’t require waiting for the response before proceeding.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  3. Avoid doing same behaviour as the original flow when it’s in dry run mode
&lt;/h4&gt;

&lt;p&gt;It sounds easy, but we need to do it with extra care. E.g.&lt;br&gt;
Make sure it’s a read-only endpoint if we don’t need to write anything in dry run mode. One way of doing this is using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SET SESSION CHARACTERISTICS AS TRANSACTION read only;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in postgres to ensure we're only doing read-only operations to db. Also we should avoid publishing any user-interaction events for analytics (if have) when it's in dry-run mode.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Avoid anything which could disrupt the original flow
&lt;/h4&gt;

&lt;p&gt;E.g. We could throw an error from the new endpoint, but we definitely don’t want to show the error to users if it’s in dry run mode; because it has nothing to do with the current flow.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Deprecate the dry run mode as part of the clean-up
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Always keep the codebase clean, we should deprecate the dry run mode afterwards by:
Removing the &lt;code&gt;dry_run_*&lt;/code&gt; columns and make sure to sync with other skateholders, make sure to clean up everything in downstream usages.&lt;/li&gt;
&lt;li&gt;Removing the code branches if it’s unnecessary after feature flag is on&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;To me, having the new logic with dry-run mode in production is a &lt;strong&gt;double-edged sword&lt;/strong&gt;. On one hand, it is helpful to ensure the business logic is correct; but on other hand, it also creates loopholes which may affect the original flow too. We just need to &lt;strong&gt;handle it with extra care&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Thus it &lt;strong&gt;may not worth&lt;/strong&gt; to do dry-run for every feature release. But it's worth considered doing so if that feature is of &lt;strong&gt;paramount importance&lt;/strong&gt; (e.g. directly tied to the company's income). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR, do at your own risk ;D&lt;/strong&gt; Hope that's helpful.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>django</category>
      <category>postgres</category>
      <category>react</category>
    </item>
    <item>
      <title>Put Overlays on image and fit in different aspect ratios</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Sat, 10 Sep 2022 05:54:17 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/put-overlays-on-image-and-fit-in-different-aspect-ratios-10hn</link>
      <guid>https://dev.to/jimmyyeung/put-overlays-on-image-and-fit-in-different-aspect-ratios-10hn</guid>
      <description>&lt;h2&gt;
  
  
  Preface
&lt;/h2&gt;

&lt;p&gt;I would not say I am a frontend guy. But most of the time in my company I need to do projects in a fullstack way, meaning that I need to touch the frontend side of work as well. And here comes an interesting problem for me to solve ;D&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;We need to highlight certain phrases of an image full of text. We're given sets of &lt;code&gt;(x,y,height,width)&lt;/code&gt; of the image. The &lt;code&gt;(x,y,height,width)&lt;/code&gt; are in percentage so that we can simply do the following to draw an overlay on the image (See example below for highlighting the text &lt;code&gt;danger&lt;/code&gt; and &lt;code&gt;science&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;overlay {
    left: x%;
    top: y%;
    height: height%;
    width: width%;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;iframe src="https://jsfiddle.net/cyclone108/46q9utsg/23//embedded//dark" width="100%" height="600"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Ok so how about next? &lt;/p&gt;

&lt;p&gt;What if we need to fit it into a viewport which has different aspect ratio? While at the same time highlighting the same thing as the original one?&lt;/p&gt;

&lt;h2&gt;
  
  
  Fit into a different viewport
&lt;/h2&gt;

&lt;p&gt;In order to do that, we need to know &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;which part of the original image should we show, and&lt;/li&gt;
&lt;li&gt;how we scale the new positions&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Assumptions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;For simplicity, assume width should always be preserved; as we're fitting a 4629/10110 image to a 700/500 viewport &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  which part of the original image should we show
&lt;/h3&gt;

&lt;p&gt;The css property &lt;code&gt;background-position&lt;/code&gt; comes into handy. And big thanks to the article from &lt;a href="https://dev.to/this-is-learning/all-you-need-to-know-about-background-position-3aac"&gt;https://dev.to/this-is-learning/all-you-need-to-know-about-background-position-3aac&lt;/a&gt;, which clearly explains how &lt;code&gt;background-position&lt;/code&gt; works. If we know how it works, we could then calculate the new positions by simple maths.&lt;/p&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;we need to know the minimum y so that we can set that as the new &lt;code&gt;background-position-y&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;then we need to calculate the new &lt;code&gt;y'&lt;/code&gt; and &lt;code&gt;height'&lt;/code&gt; after the scaling. (I'm skipping the maths here, but if you are interested, feel free to take a look at the example below)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And at last we could result in something like this ;D&lt;br&gt;
&lt;iframe src="https://jsfiddle.net/cyclone108/y1zv9ugt/27//embedded//dark" width="100%" height="600"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Sum Up
&lt;/h2&gt;

&lt;p&gt;Of course it's not perfect solution. I'm just trying to demo the css properties we can make use of when tackling a problem. There are also other handling/optimisation needed, e.g.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what if the highlight (partially) lies outside of the viewport?&lt;/li&gt;
&lt;li&gt;what if some coordinates cannot fit into the viewport with &lt;code&gt;x&lt;/code&gt; unchanged; then we need to scale &lt;code&gt;x&lt;/code&gt; too.&lt;/li&gt;
&lt;li&gt;others...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Love to hear if there're other approaches towards this too!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
      <category>todayilearned</category>
    </item>
    <item>
      <title>Check if a file is a subset of another file using bash script</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Sat, 22 Jan 2022 01:48:05 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/check-if-a-file-is-a-subset-of-another-file-using-bash-script-4ofe</link>
      <guid>https://dev.to/jimmyyeung/check-if-a-file-is-a-subset-of-another-file-using-bash-script-4ofe</guid>
      <description>&lt;h2&gt;
  
  
  Scenario
&lt;/h2&gt;

&lt;p&gt;I need to check if a file is a subset of another file into the CI pipeline. Thus bash script is chosen since it's performant and we don't need to install extra dependencies into the CI pipeline.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;diff&lt;/strong&gt;&lt;br&gt;
The first command comes to my mind is &lt;code&gt;diff&lt;/code&gt;, which is a really powerful command telling the difference between two files.&lt;/p&gt;

&lt;p&gt;However it's too powerful. &lt;strong&gt;&lt;code&gt;diff&lt;/code&gt; "predicts" which line needs to be changed in order to make the two files identical&lt;/strong&gt;; which is unnecessary for my use case.&lt;/p&gt;

&lt;p&gt;E.g. (Example from GeeksToGeeks)&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat a.txt
Gujarat
Uttar Pradesh
Kolkata
Bihar
Jammu and Kashmir

$ cat b.txt
Tamil Nadu
Gujarat
Andhra Pradesh
Bihar
Uttar pradesh

$ diff a.txt b.txt
0a1
&amp;gt; Tamil Nadu
2,3c3
&amp;lt; Uttar Pradesh
 Andhra Pradesh
5c5
 Uttar pradesh
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;comm&lt;/strong&gt;&lt;br&gt;
Without further digging into &lt;code&gt;diff&lt;/code&gt;, I found another command &lt;code&gt;comm&lt;/code&gt; which is simple and just fit in my use case.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;comm&lt;/code&gt; returns 3 columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;first column contains names &lt;strong&gt;only present in the 1st file&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;second column contains names &lt;strong&gt;only present in 2nd file&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;the third column contains &lt;strong&gt;names common to both the files&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;E.g. (Example from GeeksToGeeks)&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// displaying contents of file1 //
$cat file1.txt
Apaar 
Ayush Rajput
Deepak
Hemant

// displaying contents of file2 //
$cat file2.txt
Apaar
Hemant
Lucky
Pranjal Thakral

$comm file1.txt file2.txt
                Apaar
Ayush Rajput
Deepak
                Hemant
        Lucky
        Pranjal Thakral
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;And to check if one file is a subset of another file, we just need the 1st column. We could just do &lt;code&gt;-23&lt;/code&gt; to neglect the 2nd and 3rd column. I.e.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;comm -23 file1.txt file2.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;At last, I just end up with this simple bash script         to check the subset condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash    &lt;/span&gt;
&lt;span class="nv"&gt;SUBSET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;subset_file_path&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SUPERSET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;superset_file_path&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;CHECK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;comm&lt;/span&gt; &lt;span class="nt"&gt;-23&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nv"&gt;$SUBSET&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nv"&gt;$SUPERSET&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nv"&gt;$CHECK&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Detected extra line in &lt;/span&gt;&lt;span class="nv"&gt;$SUBSET&lt;/span&gt;&lt;span class="s2"&gt; and not in &lt;/span&gt;&lt;span class="nv"&gt;$SUPERSET&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$CHECK&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Added the extra &lt;code&gt;sort&lt;/code&gt; and &lt;code&gt;uniq&lt;/code&gt; commands there just to make sure we're comparing two sorted and deduplicated files.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>testing</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Debounced Hover on Nested Components Using Event Delegation</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Fri, 10 Dec 2021 08:20:42 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/debounced-hover-on-nested-components-using-event-delegation-18ji</link>
      <guid>https://dev.to/jimmyyeung/debounced-hover-on-nested-components-using-event-delegation-18ji</guid>
      <description>&lt;h2&gt;
  
  
  Consider the following case:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FirstLayeredElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SecondLayeredElement&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SecondLayeredElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeepestElement1&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeepestElement2&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DeepestElement1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DeepestElement2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where we want to fire a function &lt;code&gt;logAnalytics()&lt;/code&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when the cursor is hovered on a &lt;code&gt;DeepestElement&lt;/code&gt; for some seconds (e.g. 1s)&lt;/li&gt;
&lt;li&gt;and we want to know which &lt;code&gt;DeepestElement&lt;/code&gt; is captured (Consider some of the info needs to come from the parent components, so we couldn't simply add a listener in &lt;code&gt;DeepestElement&lt;/code&gt;s)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One of the approach is
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;pass &lt;code&gt;onMouseEnter&lt;/code&gt; handlers into nested div, with the use of debounce from &lt;code&gt;lodash-es&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FirstLayeredElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SecondLayeredElement&lt;/span&gt;
        &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;labelType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;logAnalytic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;labelType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Some other info&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SecondLayeredElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeepestElement1&lt;/span&gt;
       &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeepestElement2&lt;/span&gt;
       &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;     &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DeepestElement1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Delay for one second&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedMouseEnter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;
      &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;debouncedMouseEnter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;someLabelType1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DeepestElement2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Delay for one second&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedMouseEnter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;
      &lt;span class="nx"&gt;onMouseEnter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;debouncedMouseEnter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;someLabelType2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But seems lots of useless listeners are added...could we do it in a simpler way?&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Delegation Approach
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;First we define a hook &lt;code&gt;useDebounceHover&lt;/code&gt;, the input &lt;code&gt;onHover&lt;/code&gt; will be called &lt;code&gt;onMouseOut&lt;/code&gt; if the time difference between &lt;code&gt;onMouseOver&lt;/code&gt; and &lt;code&gt;onMouseOut&lt;/code&gt; &amp;gt; 1s (&lt;code&gt;onMouseEnter&lt;/code&gt; cannot be used in event delegation, check &lt;a href="https://javascript.info/mousemove-mouseover-mouseout-mouseenter-mouseleave#:~:text=Events%20mouseenter%2Fmouseleave%20are%20like,from%20descendants%2C%20are%20not%20counted"&gt;here&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseenter_event"&gt;here&lt;/a&gt; for more details)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DOMAttributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MouseEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ComponentIdToTypeMapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;some_data_id_1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;someLabelType1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;some_data_id_2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;someLabelType2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useDebounceHover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;onHover&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MouseEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DOMAttributes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onMouseOver&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onMouseOut&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;labelToHoverDurationMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;some_data_id_1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;some_data_id_2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleMouseOver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MouseEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;labelType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ComponentIdToTypeMapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;labelType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;labelToHoverDurationMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;labelType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleMouseOut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MouseEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;labelType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ComponentIdToTypeMapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;labelType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;onHover&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;labelToHoverDurationMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;labelType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;onHover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;labelToHoverDurationMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;labelType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;onMouseOver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;handleMouseOver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onMouseOut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;handleMouseOut&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;useDebounceHover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;And so you could:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FirstLayeredElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onMouseOver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMouseOut&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useDebounceHover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logAnalytic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; 
      &lt;span class="nx"&gt;onMouseOver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onMouseOver&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;onMouseOut&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onMouseOut&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
       &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SecondLayeredElement&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SecondLayeredElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeepestElement1&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeepestElement2&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DeepestElement1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DeepestElement1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DeepestElement2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DeepestElement2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The presentation layer should be simpler coz with the hook, we just need to&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add a parent div for &lt;code&gt;onMouseOver&lt;/code&gt; and &lt;code&gt;onMouseOut&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;add &lt;code&gt;data-id&lt;/code&gt; to the deepest components&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Note that &lt;a href="https://dev.to/thawsitt/should-i-use-event-delegation-in-react-nl0"&gt;React has done some optimization&lt;/a&gt; so the performance w/o event delegation are similar. &lt;strong&gt;Event Delegation does not help in performance in React.&lt;/strong&gt; But for simplicity, actually my team prefers to use Event Delegation.&lt;/p&gt;

&lt;p&gt;But again, there's always trade-off and it depends on different cases ;D.&lt;/p&gt;

</description>
      <category>react</category>
      <category>eventdelegation</category>
      <category>hover</category>
      <category>simplicity</category>
    </item>
    <item>
      <title>Performant script in updating a large table🚀</title>
      <dc:creator>Jimmy Yeung</dc:creator>
      <pubDate>Mon, 06 Sep 2021 16:50:04 +0000</pubDate>
      <link>https://dev.to/jimmyyeung/performant-script-in-updating-a-large-table-1pfa</link>
      <guid>https://dev.to/jimmyyeung/performant-script-in-updating-a-large-table-1pfa</guid>
      <description>&lt;p&gt;From time to time, it's inevitable to update some fields in the database via some manual scripts, no matter it's data patching or some random requirements from the business side.&lt;/p&gt;

&lt;p&gt;I often have a hard time in writing a script that updates a large table. It just takes so much time in running once, not to mention if you need to debug/optimise after each run. &lt;/p&gt;

&lt;h2&gt;
  
  
  Use Case
&lt;/h2&gt;

&lt;p&gt;In one of my sprint, I need to append a json object into a &lt;code&gt;jsonb&lt;/code&gt; column, in which the json object is computed differently for each row. There are &lt;strong&gt;millions&lt;/strong&gt; of records in the Postgres table and it takes hours to run the script for testing. In order to minimise the time taken for each test so that I can finish my work on time, I tried to compare different ways in order to acquire the fastest result:&lt;/p&gt;

&lt;p&gt;(In the examples below, I'm using 10000 records for each case).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Naive Django ORM &lt;code&gt;.save()&lt;/code&gt; way&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for record in records:
    json_value = &amp;lt;computation on json_value&amp;gt;
    record.json_array_field.append(json_value)
    record.save()
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;with &lt;code&gt;cProfile&lt;/code&gt;: &lt;code&gt;6951470 function calls (6891465 primitive calls) in 31.840 seconds&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is definitely the slowest way as it hits the database and executes the update query for every row. Just the first try, we could definitely make things faster.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Django Django ORM &lt;code&gt;bulk_update&lt;/code&gt; way&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for record in records:
    json_value = &amp;lt;computation on json_value&amp;gt;
    record.json_array_field.append(json_value)
MyModel.objects.bulk_update(records, fields=. ["json_array_field"])
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;with &lt;code&gt;cProfile&lt;/code&gt;: &lt;code&gt;5072220 function calls (4612182 primitive calls) in 20.213 seconds&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Slightly better. &lt;code&gt;bulk_update&lt;/code&gt; is actually suggested by Django for &lt;a href="https://docs.djangoproject.com/en/3.2/topics/db/optimization/#update-in-bulk"&gt;optimization&lt;/a&gt; as &lt;code&gt;bulk_update&lt;/code&gt; just executes one query. But it's still a lot of time, I tried to find if there's better way to improve.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Django &lt;code&gt;bulk_update&lt;/code&gt; with &lt;code&gt;batch_size&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for record in records:
    json_value = &amp;lt;computation on json_value&amp;gt;
    record.json_array_field.append(json_value)
MyModel.objects.bulk_update(
    records, 
    fields=["json_array_field"], 
    batch_size=1000
)
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;with &lt;code&gt;cProfile&lt;/code&gt;: &lt;code&gt;5077900 function calls (4617619 primitive calls) in 11.765 seconds&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Django: If updating a large number of columns in a large number of rows, the SQL generated can be very large. The batch_size parameter controls how many objects are saved in a single query.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Performance are further improved by dividing the updates into 10 batches. Not bad, but again, we could make things faster.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SQL way&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;values_to_be_updated = [(json.dumps(&amp;lt;computation on json_value&amp;gt;), i.id) for i in records]
with connection.cursor() as cursor:
    execute_values(
        cursor,
        "UPDATE my_table "
        "SET json_array_field = json_array_field::jsonb || "
        "(v.new_json_value)::jsonb FROM (VALUES %s) as v (new_json_value, bid) "
        "WHERE id = v.bid::uuid;",
        values_to_be_updated,
    )
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;with &lt;code&gt;cProfile&lt;/code&gt;: &lt;code&gt;802769 function calls (802756 primitive calls) in 3.603 seconds&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That calls it a day since it reduces down to just ~3 seconds for updating! It's one tenth of using the &lt;code&gt;.save()&lt;/code&gt; one. I just feel so dumbed on trying different ORM methods at first and spent hours waiting it finish running. I should definitely use the SQL way at first. (Imagine if we are updating millions or billions of rows, how huge the difference would be.)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Takeaway (for me)
&lt;/h2&gt;

&lt;p&gt;Maybe it's well-known already, but just want to say it again: when writing a one-off bulk insert/update script in a table with large amount of records, do try consider running a SQL at the very first beginning. That could save you a lot of time on waiting and optimising. &lt;/p&gt;

&lt;p&gt;But if you needs to consider readability or you do not care about performance, ORM could be a better choice. Just remember in terms of performance, SQL always wins ;D&lt;/p&gt;

</description>
      <category>django</category>
      <category>database</category>
      <category>sql</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
