<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Session Management on jyukki's Blog</title><link>https://jyukki.com/tags/session-management/</link><description>Recent content in Session Management on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-KR</language><lastBuildDate>Wed, 18 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/session-management/index.xml" rel="self" type="application/rss+xml"/><item><title>Go로 PostgreSQL 프록시 만들기 (64) - Active Session Dashboard</title><link>https://jyukki.com/posts/2026-03-18-pgmux-64-active-session-dashboard/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/posts/2026-03-18-pgmux-64-active-session-dashboard/</guid><description>운영 중인 모든 클라이언트 세션을 실시간으로 조회하고, 문제 세션의 쿼리를 즉시 취소하는 Admin API를 구현한다. sync.Map 기반 세션 레지스트리, cancelTarget 재활용, SQL Redaction 적용까지.</description><content:encoded><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>PostgreSQL을 운영하다 보면 &ldquo;지금 누가 뭘 하고 있는지&rdquo; 보고 싶을 때가 많다. <code>pg_stat_activity</code>가 그 역할이지만, pgmux는 프록시 레벨에서 동작하므로 백엔드 커넥션이 풀링되어 <code>pg_stat_activity</code>만으로는 <strong>어떤 클라이언트가 어떤 쿼리를 보냈는지</strong> 추적하기 어렵다.</p>
<p>Active Session Dashboard는 프록시가 관리하는 <strong>모든 클라이언트 세션</strong>의 실시간 스냅샷을 제공한다:</p>
<ul>
<li><code>GET /admin/sessions</code> — 세션 목록 (ID, 클라이언트 IP, 사용자, DB, 연결 시각, 현재 쿼리, 백엔드 주소, 트랜잭션/핀 상태)</li>
<li><code>POST /admin/sessions/{id}/cancel</code> — 특정 세션의 활성 쿼리 취소</li>
</ul>
<hr>
<h2 id="설계">설계</h2>
<h3 id="세션-레지스트리">세션 레지스트리</h3>
<p>각 클라이언트 연결은 <code>handleConn</code>에서 시작하고 끝난다. 이 생명주기에 맞춰 세션을 등록/해제한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>Accept → handleConn → [Auth] → registerSession → relayQueries → unregisterSession → Close
</span></span></code></pre></div><p>레지스트리는 <code>sync.Map</code>을 사용한다. 읽기(admin 조회)가 쓰기(세션 등록/해제)보다 훨씬 빈번하고, 키 집합이 안정적인 패턴이라 <code>sync.Map</code>이 적합하다.</p>
<h3 id="backendaddr-canceltarget-재활용">backendAddr: cancelTarget 재활용</h3>
<p>세션이 쿼리를 실행하는 동안 어떤 백엔드에 연결되어 있는지가 중요하다. 이미 <code>cancelTarget</code>이 <code>setFromConn</code>/<code>clear</code>로 백엔드 주소를 정확히 추적하고 있었다.</p>
<p>처음에는 <code>SessionInfo</code>에 별도 <code>backendAddr</code> 필드를 두고 쿼리 시작/종료마다 업데이트하려 했다. 코드 리뷰에서 이 접근의 문제를 발견했다:</p>
<ol>
<li>쿼리 시작 시점에는 아직 커넥션을 획득하지 않아 주소를 모른다</li>
<li>Reader 경로는 <code>handleReadQueryTraced</code> 내부에서 주소가 결정되므로 호출자가 알 수 없다</li>
<li>Extended Query의 다양한 경로(boundReader, boundWriter, 신규 획득)마다 주소 설정이 필요하다</li>
</ol>
<p><strong>해결: cancelTarget에서 직접 파생</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">func</span> (si <span style="color:#ff79c6">*</span>SessionInfo) <span style="color:#50fa7b">Snapshot</span>() SessionSnapshot {
</span></span><span style="display:flex;"><span>    si.mu.<span style="color:#50fa7b">RLock</span>()
</span></span><span style="display:flex;"><span>    snap <span style="color:#ff79c6">:=</span> SessionSnapshot{<span style="color:#ff79c6">...</span>}  <span style="color:#6272a4">// 쿼리, 트랜잭션 등 복사</span>
</span></span><span style="display:flex;"><span>    si.mu.<span style="color:#50fa7b">RUnlock</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6272a4">// 백엔드 주소는 cancelTarget에서 파생 (자체 mutex 보호)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> si.ct <span style="color:#ff79c6">!=</span> <span style="color:#ff79c6">nil</span> {
</span></span><span style="display:flex;"><span>        addr, _, _ <span style="color:#ff79c6">:=</span> si.ct.<span style="color:#50fa7b">get</span>()
</span></span><span style="display:flex;"><span>        snap.BackendAddr = addr
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">return</span> snap
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>ct</code>는 생성 후 불변이므로 세션 락 없이 접근한다. <code>ct.get()</code>은 자체 mutex로 보호된다. 쿼리 실행 중에는 주소가 있고, 유휴 상태에서는 빈 문자열이다 — <code>pg_stat_activity</code>와 동일한 의미론이다.</p>
<hr>
<h2 id="핵심-구현">핵심 구현</h2>
<h3 id="sessioninfo-구조체">SessionInfo 구조체</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">type</span> SessionInfo <span style="color:#8be9fd;font-style:italic">struct</span> {
</span></span><span style="display:flex;"><span>    mu             sync.RWMutex
</span></span><span style="display:flex;"><span>    ID             <span style="color:#8be9fd">uint32</span>        <span style="color:#6272a4">// proxyPID (고유)</span>
</span></span><span style="display:flex;"><span>    ClientAddr     <span style="color:#8be9fd">string</span>        <span style="color:#6272a4">// 클라이언트 IP:port</span>
</span></span><span style="display:flex;"><span>    User           <span style="color:#8be9fd">string</span>
</span></span><span style="display:flex;"><span>    Database       <span style="color:#8be9fd">string</span>
</span></span><span style="display:flex;"><span>    ConnectedAt    time.Time
</span></span><span style="display:flex;"><span>    currentQuery   <span style="color:#8be9fd">string</span>        <span style="color:#6272a4">// 현재 실행 중인 SQL</span>
</span></span><span style="display:flex;"><span>    queryStartedAt time.Time     <span style="color:#6272a4">// 쿼리 시작 시각</span>
</span></span><span style="display:flex;"><span>    inTransaction  <span style="color:#8be9fd">bool</span>
</span></span><span style="display:flex;"><span>    pinned         <span style="color:#8be9fd">bool</span>
</span></span><span style="display:flex;"><span>    pinnedReason   <span style="color:#8be9fd">string</span>
</span></span><span style="display:flex;"><span>    ct             <span style="color:#ff79c6">*</span>cancelTarget <span style="color:#6272a4">// immutable after construction</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>불변 필드(ID, ClientAddr, User, Database, ConnectedAt, ct)는 생성 시 설정하고 이후 변경하지 않는다. 가변 필드(currentQuery, queryStartedAt, inTransaction 등)만 <code>RWMutex</code>로 보호한다.</p>
<h3 id="쿼리-상태-추적">쿼리 상태 추적</h3>
<p><code>relayQueries</code>의 Simple Query와 Extended Query(Sync) 양쪽에 훅을 건다:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#6272a4">// Simple Query</span>
</span></span><span style="display:flex;"><span>si.<span style="color:#50fa7b">SetQueryState</span>(query)         <span style="color:#6272a4">// 쿼리 시작</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// ... 실행 ...</span>
</span></span><span style="display:flex;"><span>si.<span style="color:#50fa7b">ClearQueryState</span>()            <span style="color:#6272a4">// 쿼리 종료</span>
</span></span><span style="display:flex;"><span>si.<span style="color:#50fa7b">SetTransactionState</span>(session.<span style="color:#50fa7b">InTransaction</span>(), session.<span style="color:#50fa7b">Pinned</span>(), session.<span style="color:#50fa7b">PinnedReason</span>())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// Extended Query (Sync)</span>
</span></span><span style="display:flex;"><span>si.<span style="color:#50fa7b">SetQueryState</span>(extQueryText)  <span style="color:#6272a4">// 배치 시작</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// ... 실행 ...</span>
</span></span><span style="display:flex;"><span>si.<span style="color:#50fa7b">ClearQueryState</span>()            <span style="color:#6272a4">// 배치 종료</span>
</span></span><span style="display:flex;"><span>si.<span style="color:#50fa7b">SetTransactionState</span>(<span style="color:#ff79c6">...</span>)
</span></span></code></pre></div><h3 id="세션-취소">세션 취소</h3>
<p>기존 CancelRequest 메커니즘을 그대로 재활용한다:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">func</span> (s <span style="color:#ff79c6">*</span>Server) <span style="color:#50fa7b">CancelSession</span>(id <span style="color:#8be9fd">uint32</span>) (found, cancelled <span style="color:#8be9fd">bool</span>) {
</span></span><span style="display:flex;"><span>    val, ok <span style="color:#ff79c6">:=</span> s.sessions.<span style="color:#50fa7b">Load</span>(id)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> !ok { <span style="color:#ff79c6">return</span> <span style="color:#ff79c6">false</span>, <span style="color:#ff79c6">false</span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    si <span style="color:#ff79c6">:=</span> val.(<span style="color:#ff79c6">*</span>SessionInfo)
</span></span><span style="display:flex;"><span>    ct <span style="color:#ff79c6">:=</span> si.ct  <span style="color:#6272a4">// immutable — no lock</span>
</span></span><span style="display:flex;"><span>    addr, bPID, bSecret <span style="color:#ff79c6">:=</span> ct.<span style="color:#50fa7b">get</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> addr <span style="color:#ff79c6">==</span> <span style="color:#f1fa8c">&#34;&#34;</span> <span style="color:#ff79c6">||</span> bPID <span style="color:#ff79c6">==</span> <span style="color:#bd93f9">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span> <span style="color:#ff79c6">true</span>, <span style="color:#ff79c6">false</span>  <span style="color:#6272a4">// 실행 중인 쿼리 없음</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> err <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">forwardCancel</span>(addr, bPID, bSecret); err <span style="color:#ff79c6">!=</span> <span style="color:#ff79c6">nil</span> {
</span></span><span style="display:flex;"><span>        slog.<span style="color:#50fa7b">Warn</span>(<span style="color:#f1fa8c">&#34;session cancel forward failed&#34;</span>, <span style="color:#f1fa8c">&#34;session_id&#34;</span>, id, <span style="color:#f1fa8c">&#34;error&#34;</span>, err)
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span> <span style="color:#ff79c6">true</span>, <span style="color:#ff79c6">false</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">return</span> <span style="color:#ff79c6">true</span>, <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>PostgreSQL CancelRequest 프로토콜을 사용하므로 백엔드가 실제로 쿼리를 취소한다.</p>
<hr>
<h2 id="admin-api-엔드포인트">Admin API 엔드포인트</h2>
<h3 id="get-adminsessions">GET /admin/sessions</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -H <span style="color:#f1fa8c">&#34;Authorization: Bearer admin-key&#34;</span> http://localhost:9091/admin/sessions
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;sessions&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;id&#34;</span>: <span style="color:#bd93f9">1</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;client_addr&#34;</span>: <span style="color:#f1fa8c">&#34;192.168.1.10:54321&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;user&#34;</span>: <span style="color:#f1fa8c">&#34;app_user&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;database&#34;</span>: <span style="color:#f1fa8c">&#34;mydb&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;connected_at&#34;</span>: <span style="color:#f1fa8c">&#34;2026-03-18T10:30:00Z&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;current_query&#34;</span>: <span style="color:#f1fa8c">&#34;SELECT * FROM users WHERE id = $1&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;query_started_at&#34;</span>: <span style="color:#f1fa8c">&#34;2026-03-18T10:35:42.123Z&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;backend_addr&#34;</span>: <span style="color:#f1fa8c">&#34;10.0.0.1:5432&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;in_transaction&#34;</span>: <span style="color:#ff79c6">false</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;pinned&#34;</span>: <span style="color:#ff79c6">false</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;id&#34;</span>: <span style="color:#bd93f9">2</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;client_addr&#34;</span>: <span style="color:#f1fa8c">&#34;192.168.1.20:54322&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;user&#34;</span>: <span style="color:#f1fa8c">&#34;admin_user&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;database&#34;</span>: <span style="color:#f1fa8c">&#34;mydb&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;connected_at&#34;</span>: <span style="color:#f1fa8c">&#34;2026-03-18T10:20:00Z&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;in_transaction&#34;</span>: <span style="color:#ff79c6">true</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;pinned&#34;</span>: <span style="color:#ff79c6">true</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;pinned_reason&#34;</span>: <span style="color:#f1fa8c">&#34;LISTEN&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li>세션 목록은 <strong>ID 오름차순</strong>으로 정렬된다 (<code>sync.Map</code> 순회 순서 비결정적이므로)</li>
<li><code>current_query</code>에는 <code>observability.sql_redaction</code> 정책이 적용된다</li>
<li>유휴 세션의 <code>current_query</code>, <code>query_started_at</code>, <code>backend_addr</code>는 omitempty로 생략</li>
</ul>
<h3 id="post-adminsessionsidcancel">POST /admin/sessions/{id}/cancel</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -X POST -H <span style="color:#f1fa8c">&#34;Authorization: Bearer admin-key&#34;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c"></span>  http://localhost:9091/admin/sessions/1/cancel
</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>상황</th>
          <th>응답 코드</th>
          <th>응답</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>취소 성공</td>
          <td>200</td>
          <td><code>{&quot;status&quot;: &quot;cancelled&quot;}</code></td>
      </tr>
      <tr>
          <td>실행 중인 쿼리 없음</td>
          <td>200</td>
          <td><code>{&quot;status&quot;: &quot;no_active_query&quot;, &quot;message&quot;: &quot;...&quot;}</code></td>
      </tr>
      <tr>
          <td>세션 없음</td>
          <td>404</td>
          <td><code>{&quot;error&quot;: &quot;session not found&quot;}</code></td>
      </tr>
      <tr>
          <td>잘못된 경로</td>
          <td>400</td>
          <td><code>{&quot;error&quot;: &quot;invalid path, expected /admin/sessions/{id}/cancel&quot;}</code></td>
      </tr>
  </tbody>
</table>
<h3 id="권한">권한</h3>
<table>
  <thead>
      <tr>
          <th>엔드포인트</th>
          <th>필요 역할</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GET /admin/sessions</td>
          <td>viewer</td>
      </tr>
      <tr>
          <td>POST /admin/sessions/{id}/cancel</td>
          <td>admin</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="sql-redaction-적용">SQL Redaction 적용</h2>
<p><code>current_query</code>는 실행 중인 원본 SQL을 담고 있다. Admin API 응답으로 내보내기 전에 <code>observability.sql_redaction</code> 정책을 적용한다:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#ff79c6">if</span> redactFn <span style="color:#ff79c6">!=</span> <span style="color:#ff79c6">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> snapshots, ok <span style="color:#ff79c6">:=</span> sessions.([]proxy.SessionSnapshot); ok {
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">for</span> i <span style="color:#ff79c6">:=</span> <span style="color:#ff79c6">range</span> snapshots {
</span></span><span style="display:flex;"><span>            <span style="color:#ff79c6">if</span> snapshots[i].CurrentQuery <span style="color:#ff79c6">!=</span> <span style="color:#f1fa8c">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>                snapshots[i].CurrentQuery = <span style="color:#50fa7b">redactFn</span>(snapshots[i].CurrentQuery)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li><code>none</code>: 원본 SQL 그대로 노출</li>
<li><code>literals</code> (기본): <code>SELECT * FROM users WHERE password = $1</code></li>
<li><code>full</code>: <code>[fingerprint:abc123]</code></li>
</ul>
<p>프록시 내부 라우팅/캐싱에는 원본 SQL을 사용하므로 기능에 영향 없다.</p>
<hr>
<h2 id="코드-리뷰에서-발견한-이슈-3건">코드 리뷰에서 발견한 이슈 3건</h2>
<table>
  <thead>
      <tr>
          <th>심각도</th>
          <th>이슈</th>
          <th>수정</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Critical</strong></td>
          <td><code>backendAddr</code> 항상 빈 문자열</td>
          <td>cancelTarget에서 직접 파생으로 변경</td>
      </tr>
      <tr>
          <td><strong>Major</strong></td>
          <td><code>CancelSession</code>이 <code>forwardCancel</code> 에러를 삼킴</td>
          <td><code>slog.Warn</code> 추가</td>
      </tr>
      <tr>
          <td><strong>Minor</strong></td>
          <td><code>QueryStartedAt</code>이 <code>string</code> (RFC3339 초 단위)</td>
          <td><code>*time.Time</code>으로 변경, ConnectedAt과 일관성 확보</td>
      </tr>
  </tbody>
</table>
<p>Critical은 모든 경로(Simple/Extended/Reader/Writer)에서 <code>backend_addr</code>가 항상 빈 값으로 나오는 버그였다. 기존 <code>cancelTarget</code>이 이미 정확한 주소를 추적하고 있었으므로, 중복 추적 대신 파생으로 전환하여 문제를 해결했다.</p>
<hr>
<h2 id="테스트">테스트</h2>
<p>17개 테스트로 커버:</p>
<p><strong>proxy 패키지 (7건)</strong></p>
<ul>
<li>SetQueryState / ClearQueryState 상태 전이</li>
<li>cancelTarget에서 backendAddr 파생</li>
<li>트랜잭션/핀 상태 업데이트</li>
<li>세션 레지스트리 CRUD + ID 정렬</li>
<li>CancelSession (미존재, 쿼리 없음)</li>
<li>동시성 접근 (race detector)</li>
</ul>
<p><strong>admin 패키지 (10건)</strong></p>
<ul>
<li>빈 세션 목록, 활성 세션 응답 형식</li>
<li>SQL Redaction 적용</li>
<li>MethodNotAllowed, NotConfigured</li>
<li>Cancel 성공/NotFound/NoActiveQuery</li>
<li>잘못된 경로 형식</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>기존에 구현해둔 <code>cancelTarget</code>(CancelRequest 프로토콜)을 세션 대시보드에 재활용한 것이 이번 작업의 핵심이다. 쿼리 실행 중 백엔드 주소를 정확히 추적하는 메커니즘이 이미 있었으므로, 새로 만들 필요 없이 Snapshot에서 파생하는 것만으로 충분했다.</p>
<p>코드 리뷰에서 <code>backendAddr</code>가 항상 빈 값인 Critical 버그를 잡은 것도 의미가 있다. 처음 구현에서는 &ldquo;쿼리 시작 시 주소를 함께 전달&quot;하려 했지만, 커넥션 획득은 쿼리 분류 후에 일어나므로 시점이 맞지 않았다. 이미 존재하는 인프라를 재활용하는 것이 새로 만드는 것보다 정확하고 유지보수 비용도 낮다.</p>
<p>다음은 <strong>Per-User Rate Limiting</strong>으로 멀티테넌트 환경의 noisy neighbor 문제를 해결할 예정이다.</p>
]]></content:encoded></item></channel></rss>