<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Makridenko's blog</title>
    <link>https://makridenko.com</link>
    <description>Мои технические и не очень заметки.</description>
    <language>ru-ru</language>
    <lastBuildDate>Fri, 18 Sep 2026 11:49:33 GMT</lastBuildDate>
    <atom:link href="https://makridenko.com/rss.xml" rel="self" type="application/rss+xml"/>
    
    <item>
      <title><![CDATA[Переезд на makridenko.com]]></title>
      <link>https://makridenko.com/posts/2026/09/18/move-to-makridenkocom</link>
      <guid>https://makridenko.com/posts/2026/09/18/move-to-makridenkocom</guid>
      <description><![CDATA[Блог переезжает на makridenko.com — подпишитесь на новый RSS-фид.]]></description>
      <content:encoded><![CDATA[<p>В связи с последними событиями блог переезжает на <a href="https://makridenko.com">makridenko.com</a>. Все ссылки будут делать редирект <code>.ru</code> -&gt; <code>.com</code>, но вот RSS работать не будет, поэтому советую подписаться на новый фид: <a href="https://makridenko.com/rss.xml">makridenko.com/rss.xml</a></p>]]></content:encoded>
      <pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <category>Random</category>
    </item>
    <item>
      <title><![CDATA[Jam 4.0.0]]></title>
      <link>https://makridenko.com/posts/2026/09/16/jam400</link>
      <guid>https://makridenko.com/posts/2026/09/16/jam400</guid>
      <description><![CDATA[Релиз Jam 4.0.0 что поменялось и куда движемся дальше]]></description>
      <content:encoded><![CDATA[<p>Вышел <a href="https://github.com/mkrdnk/jam/releases/tag/v4.0.0">Jam 4.0.0</a>.</p>
<p>Последние несколько месяцев я постепенно переделывал основную концепцию библиотеки. В итоге изменений накопилось достаточно много, поэтому 4.0 это не просто очередной набор новых фич, а довольно серьёзный пересмотр того, чем вообще должен быть Jam.</p>
<p>Если коротко: раньше Jam был скорее набором отдельных инструментов для authentication, а теперь постепенно превращается в полноценный authentication/authorization framework.</p>
<h2>jam.Jam</h2>
<p>Самое заметное изменение - полностью переделанный <code>jam.Jam</code>-фасад.</p>
<p>Раньше он был скорее удобной надстройкой над отдельными модулями:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam

jam = Jam(config=config)

jwt_token = jam.jwt_encode(payload={<span class="hljs-string">&quot;sub&quot;</span>: <span class="hljs-number">1</span>})
payload = jam.jwt_decode(token=jwt_token)
</code></pre>
<p>Это работало, но в таком API приложение всё равно фактически знает, что оно использует JWT.</p>
<p>В 4.0 я хотел уйти от этого уровня абстракции. Приложение должно работать с понятиями auth (x/z), а конкретный механизм: jose, paseto, session и тд -  должен оставаться деталью реализации.</p>
<p>Поэтому теперь основной flow выглядит примерно так:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> dataclasses <span class="hljs-keyword">import</span> dataclass

<span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam, BaseSubject


<span class="hljs-meta">@dataclass</span>
<span class="hljs-keyword">class</span> <span class="hljs-title class_">User</span>(<span class="hljs-title class_ inherited__">BaseSubject</span>):
    <span class="hljs-built_in">id</span>: <span class="hljs-built_in">int</span>
    email: <span class="hljs-built_in">str</span>
    role: <span class="hljs-built_in">str</span> = <span class="hljs-string">&quot;user&quot;</span>


jam = Jam(config=config)

user = User(
    <span class="hljs-built_in">id</span>=<span class="hljs-number">1</span>,
    email=<span class="hljs-string">&quot;some@mail.com&quot;</span>,
)

<span class="hljs-comment"># выпускаем credentials для пользователя</span>
token = jam.issue(subject=user)

<span class="hljs-comment"># аутентифицируем credentials</span>
principal = jam.authenticate(token=token)

<span class="hljs-keyword">assert</span> principal.subject.<span class="hljs-built_in">id</span> == user.<span class="hljs-built_in">id</span>

<span class="hljs-comment"># и выполняем авторизацию</span>
allowed = jam.authorize(
    principal=principal,
    permission=<span class="hljs-string">&quot;post:delete&quot;</span>,
)
</code></pre>
<p>Здесь появляются две важные сущности: <code>Subject</code> и <code>Principal</code>.</p>
<p><code>Subject</code> это сущность, от имени которой выполняется authentication. В простейшем случае это пользователь, но это не обязательно должен быть именно пользователь, например какая-то виртуальная машина, которая запрашивает доступы к определенным ресурсам.</p>
<p><code>Principal</code> результат authentication. Он содержит subject и информацию, необходимую для дальнейшей авторизации.</p>
<p>Мне такой подход нравится гораздо больше: конкретный механизм authentication становится заменяемой деталью, а application code работает с единой моделью.</p>
<h2>Интеграции с фреймворками</h2>
<p>Вместе с этим переработал интеграции с фреймворками.</p>
<p>Например, с FastAPI теперь это выглядит так:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Annotated

<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> Depends, FastAPI

<span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam
<span class="hljs-keyword">from</span> jam.authz <span class="hljs-keyword">import</span> Principal
<span class="hljs-keyword">from</span> jam.ext.fastapi <span class="hljs-keyword">import</span> JamAuth


jam = Jam(<span class="hljs-string">&quot;config.toml&quot;</span>)
auth = JamAuth(jam)

app = FastAPI()


<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/me&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">me</span>(<span class="hljs-params">
    principal: Annotated[
        Principal,
        Depends(<span class="hljs-params">auth</span>), <span class="hljs-comment"># получаем principal</span>
    ],
</span>):
    <span class="hljs-keyword">return</span> principal.subject


<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/landing&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">landing</span>(<span class="hljs-params">
    principal: Annotated[
        Principal | <span class="hljs-literal">None</span>,
        Depends(<span class="hljs-params">auth.optional</span>), <span class="hljs-comment"># не обязательная аутенфикация</span>
    ],
</span>):
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">&quot;authenticated&quot;</span>: principal <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>,
    }


<span class="hljs-meta">@app.patch(<span class="hljs-params"><span class="hljs-string">&quot;/posts/{post_id}&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">edit_post</span>(<span class="hljs-params">
    post_id: <span class="hljs-built_in">str</span>,
    principal: Annotated[
        Principal,
        Depends(<span class="hljs-params">auth.require(<span class="hljs-params"><span class="hljs-string">&quot;post:edit&quot;</span></span>)</span>), <span class="hljs-comment"># конкретный пермишен</span>
    ],
</span>):
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">&quot;post_id&quot;</span>: post_id,
        <span class="hljs-string">&quot;editor&quot;</span>: principal.subject,
    }
</code></pre>
<p>При этом application code получает уже готовый <code>Principal</code>, а не разбирается с заголовками, токенами и конкретным механизмом authentication.</p>
<p>Можно просто получить текущего пользователя:</p>
<pre><code class="hljs language-python">Depends(auth)
</code></pre>
<p>Можно разрешить anonymous access:</p>
<pre><code class="hljs language-python">Depends(auth.optional)
</code></pre>
<p>А можно сразу потребовать permission:</p>
<pre><code class="hljs language-python">Depends(auth.require(<span class="hljs-string">&quot;post:edit&quot;</span>))
</code></pre>
<p>То есть authorization можно достаточно естественно встроить прямо в application flow.</p>
<h2>При этом отдельные механизмы никуда не делись</h2>
<p>При всём этом я не хотел превращать Jam в монолит.</p>
<p>Отдельные модули по-прежнему можно использовать независимо друг от друга. Если вам нужен только конкретный механизм, можно работать с ним напрямую. Если же хочется построить поверх Jam полноценный authentication flow - для этого теперь есть <code>Jam</code>.</p>
<p>Для меня это довольно важная часть архитектуры: модули должны оставаться заменяемыми и не зависеть друг от друга больше необходимого.</p>
<h2>Keychain</h2>
<p>Но киллер-фичей этого релиза я всё-таки считаю keychain.
<a href="https://makridenko.com/posts/2026/09/08/keychains-in-authxz-frameworks">Я уже писал про эту идею раньше</a>, но за время реализации механизм немного переработал.
Проблема, которую я хотел решить, довольно простая. Приложение не должно само заниматься хранением и ротацией криптографических ключей.</p>
<p>Например, для JWT можно указать keychain в конфигурации:</p>
<pre><code class="hljs language-toml"><span class="hljs-section">[jam.jose.jwt]</span>
<span class="hljs-attr">alg</span> = <span class="hljs-string">&quot;RS256&quot;</span>
<span class="hljs-attr">keychain</span> = <span class="hljs-string">&quot;jose_keys&quot;</span>

<span class="hljs-section">[jam.keychains.jose_keys]</span>
<span class="hljs-comment"># Пока доступны только FileStorage и MemoryStorage.</span>
<span class="hljs-comment"># В будущем хочу добавить HashiCorp Vault,</span>
<span class="hljs-comment"># а остальные варианты можно будет реализовать</span>
<span class="hljs-comment"># через публичный интерфейс.</span>
<span class="hljs-attr">type</span> = <span class="hljs-string">&quot;FileStorage&quot;</span>
<span class="hljs-attr">path</span> = <span class="hljs-string">&quot;/opt/keys&quot;</span>
</code></pre>
<p>После этого приложение работает уже с keychain:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam
<span class="hljs-keyword">from</span> jam.keychain <span class="hljs-keyword">import</span> FileStorage


jam = Jam(config=<span class="hljs-string">&quot;config.toml&quot;</span>)

keychain: FileStorage = jam.keychains[<span class="hljs-string">&quot;jose_keys&quot;</span>]

<span class="hljs-comment"># Добавляем ключ.</span>
<span class="hljs-comment">#</span>
<span class="hljs-comment"># Если material не передавать,</span>
<span class="hljs-comment"># Jam сам сгенерирует ключ</span>
<span class="hljs-comment"># исходя из конфигурации.</span>
keychain.add(
    key_id=<span class="hljs-string">&quot;my-key-id&quot;</span>,
)

<span class="hljs-comment"># Получаем текущий ключ.</span>
keychain.current(key_id=<span class="hljs-string">&quot;my-key-id&quot;</span>)

<span class="hljs-comment"># Выпускаем credentials.</span>
token = jam.issue(
    subject={<span class="hljs-string">&quot;sub&quot;</span>: <span class="hljs-number">1</span>},
)

<span class="hljs-comment"># Ротируем ключ.</span>
keychain.rotate()
</code></pre>
<p>Здесь важна не столько сама возможность вызвать <code>rotate()</code>, сколько то, что приложение вообще не обязано знать, где физически хранятся ключи и какой из них сейчас используется.</p>
<p>Например, представим, что приложение несколько месяцев работает в production, а потом возникает необходимость заменить приватный ключ. Без keychain это легко превращается в ручную процедуру: найти файлы с ключами, заменить их, разобраться с уже выпущенными токенами и не сломать валидацию старых credentials. С keychain эта логика становится частью инфраструктуры authentication. Новый ключ можно сделать текущим, а старые ключи при этом оставить доступными для проверки уже выпущенных токенов.</p>
<p>То есть rotation не должен автоматически означать: <em>старые токены перестали работать прямо сейчас.</em></p>
<p>Это особенно полезно для распределённых приложений, где несколько экземпляров сервиса могут какое-то время находиться на разных версиях конфигурации.</p>
<h2>Управление через CLI</h2>
<p>Keychain&#x27;ом можно так же управлять из консоли, во время запущенного приложения.</p>
<p>Например:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="bash">pip install jamlib[cli]</span>
<span class="hljs-meta prompt_">
$ </span><span class="bash">jam keychain --config jam.toml rotate jose_keys</span>
<span class="hljs-meta prompt_">
$ </span><span class="bash">jam keychain --config jam.toml list jose_keys</span>
</code></pre>
<p>Это позволяет выполнять операции с ключами без необходимости писать отдельный deployment script или лезть непосредственно в storage.</p>
<p>Пока реализованы два storage:</p>
<ul>
<li><code>FileStorage</code></li>
<li><code>MemoryStorage</code></li>
</ul>
<p>Дальше хочу расширять эту часть. В частности, планирую добавить интеграцию с HashiCorp Vault.</p>
<p>При этом я не хочу делать отдельную реализацию для каждого возможного secret storage. Для остальных вариантов будет публичный интерфейс, через который можно будет реализовать собственный backend.</p>
<h2>Что изменилось с точки зрения архитектуры</h2>
<p>Наверное, самое важное изменение 4.0.0: Jam постепенно перестаёт быть просто библиотекой с набором auth-инструментов.</p>
<p>Условно раньше application code выглядел примерно так:</p>
<pre><code class="hljs language-text">Application
    |
    +-- JWT
    +-- PASETO
    +-- Sessions
    +-- OAuth2
    +-- ...
</code></pre>
<p>Теперь я хочу двигаться к модели:</p>
<pre><code class="hljs language-text">Application
    |
    +-- Jam
          |
          +-- Authentication
          |
          +-- Authorization
          |
          +-- JWT
          +-- PASETO
          +-- Sessions
          +-- OAuth2
          +-- ...
</code></pre>
<p>При этом конкретные механизмы остаются модулями, которые можно использовать независимо. Получается некоторый слой абстракции над authentication, но без попытки спрятать вообще всё за одним огромным классом.</p>
<h2>Что дальше?</h2>
<p>В ближайших планах:</p>
<ul>
<li>расширение keychain на другие модули</li>
<li>hashicorp storage</li>
<li>saml в основном фасаде <code>jam.Jam</code></li>
<li>macaroons + biscuits токены</li>
<li>расширение возможности authz настроек</li>
</ul>
<p><a href="https://github.com/mkrdnk/jam/releases/tag/v4.0.0">GitHub Release</a> | <a href="https://jam.makridenko.ru">Documentation</a></p>]]></content:encoded>
      <pubDate>Wed, 16 Sep 2026 00:00:00 GMT</pubDate>
      <author>undefined</author>
      <category>Python</category>
      <category>Devlog</category>
    </item>
    <item>
      <title><![CDATA[Ещё одна вещь, которой не хватает auth-фреймворкам]]></title>
      <link>https://makridenko.com/posts/2026/09/08/keychains-in-authxz-frameworks</link>
      <guid>https://makridenko.com/posts/2026/09/08/keychains-in-authxz-frameworks</guid>
      <description><![CDATA[Когда речь заходит о JWT или PASETO, все обсуждают токены, но почти никто не говорит об управлении ключами, а зря!]]></description>
      <content:encoded><![CDATA[<p>Когда-то давно <a href="https://radio-t.com">Лёха из Радио-Т</a> жаловался на одну вещь в Python-инструментах для аутентификации и авторизации - отсутствие какого-либо keychain-механизма для управления криптографическими ключами.</p>
<p>Я и сам давно сталкиваюсь с этой проблемой. Практически в каждом проекте приходится заново реализовывать ротацию ключей, хранение их истории, поддержку отозванных ключей и обработку ситуации, когда ключ уже заменён, но выпущенные ранее токены всё ещё должны считаться валидными.</p>
<p>JWT, PASETO и другие форматы токенов обычно решают задачу подписи и верификации данных, но управление самими ключами почти всегда остаётся на совести разработчика. В результате каждый проект изобретает собственный велосипед.</p>
<p>Со временем Jam перестал быть просто библиотекой для работы с токенами и постепенно превратился в полноценный auth-фреймворк: появились принципалы и субъекты аутентификации, собственный DSL для описания прав доступа (об этом после релиза уже :D ), унифицированные интерфейсы для различных механизмов авторизации. Поэтому появление keychain выглядело вполне логичным следующим шагом.</p>
<p>Основная идея проста: предоставить абстракцию хранилища ключей и единый интерфейс работы с ним. Пользователь может использовать встроенные реализации или написать собственную — например, для хранения ключей в PostgreSQL, HashiCorp Vault, AWS KMS, Redis или любом другом бэкенде.</p>
<p>На данный момент Jam поставляется с двумя реализациями:</p>
<ul>
<li><code>FileStorage</code>: хранение ключей на файловой системе;</li>
<li><code>MemoryStorage</code>: хранение ключей в памяти процесса.</li>
</ul>
<p>В текущей (ещё не выпущенной) версии Jam 4.0.0 это выглядит так.</p>
<p>Сначала в конфигурации создаётся один или несколько keychain&#x27;ов:</p>
<pre><code class="hljs language-toml"><span class="hljs-section">[jam.keychains.keychain_name]</span>
<span class="hljs-attr">type</span> = <span class="hljs-string">&quot;FileStorage&quot;</span>
<span class="hljs-attr">path</span> = <span class="hljs-string">&quot;./keychain&quot;</span>

<span class="hljs-section">[jam.paseto]</span>
<span class="hljs-attr">version</span> = <span class="hljs-string">&quot;v4&quot;</span>
<span class="hljs-attr">purpose</span> = <span class="hljs-string">&quot;local&quot;</span>
<span class="hljs-attr">keychain</span> = <span class="hljs-string">&quot;keychain_name&quot;</span>
</code></pre>
<p>После этого PASETO или сам Jam будут автоматически получать ключи из указанного keychain&#x27;а.</p>
<p>Пример прямого использования PASETO:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam.paseto <span class="hljs-keyword">import</span> PASETOv4

paseto = PASETOv4.key(
    config=<span class="hljs-string">&quot;config.toml&quot;</span>
)

token = paseto.encode(
    payload={
        <span class="hljs-string">&quot;sub&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>,
        <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;admin&quot;</span>
    },
    footer=<span class="hljs-literal">None</span>
)
</code></pre>
<p>Или через общий интерфейс Jam:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam

jam = Jam(config=<span class="hljs-string">&quot;config.toml&quot;</span>)

token = jam.issue(
    sub={<span class="hljs-string">&quot;sub&quot;</span>: <span class="hljs-string">&quot;1&quot;</span>, <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;admin&quot;</span>},
    via=<span class="hljs-string">&quot;paseto&quot;</span>
)
</code></pre>
<p>Возникает вопрос: а как добавлять новые ключи и выполнять их ротацию?</p>
<p>Для этого предусмотрены два способа. Первый подходит для встраивания в бизнес-логику приложения, второй — для администрирования через CLI.</p>
<p>Через Python API:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam.keychain <span class="hljs-keyword">import</span> FileStorage

keychain = FileStorage(
    path=<span class="hljs-string">&quot;./keychain&quot;</span>,
    name=<span class="hljs-string">&quot;keychain_name&quot;</span>
)

<span class="hljs-comment"># добавить новый ключ</span>
keychain.add(
    <span class="hljs-built_in">id</span>=<span class="hljs-string">&quot;new-key-id&quot;</span>
)

<span class="hljs-comment"># сделать его активным</span>
keychain.current(
    <span class="hljs-built_in">id</span>=<span class="hljs-string">&quot;new-key-id&quot;</span>
)

<span class="hljs-comment"># выполнить ротацию</span>
keychain.rotate()

<span class="hljs-comment"># удалить ключ</span>
keychain.remove(
    <span class="hljs-built_in">id</span>=<span class="hljs-string">&quot;new-key-id&quot;</span>
)
</code></pre>
<p>Через Jam CLI:</p>
<pre><code class="hljs language-bash"><span class="hljs-comment"># установить CLI</span>
pip install jamlib[cli]

<span class="hljs-comment"># добавить ключ</span>
jam keychain --config config.toml add keychain_name new-key-id

<span class="hljs-comment"># сделать активным</span>
jam keychain --config config.toml current keychain_name new-key-id

<span class="hljs-comment"># выполнить ротацию</span>
jam keychain --config config.toml rotate keychain_name

<span class="hljs-comment"># удалить ключ</span>
jam keychain --config config.toml remove keychain_name new-key-id
</code></pre>
<p>Однако самое интересное здесь не встроенные реализации, а возможность написать собственный keychain.</p>
<p>Например, если ключи должны храниться в PostgreSQL:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam.keychain <span class="hljs-keyword">import</span> BaseKeychain
<span class="hljs-keyword">from</span> jam.keychain.models <span class="hljs-keyword">import</span> Key


<span class="hljs-keyword">class</span> <span class="hljs-title class_">PostgresKeychain</span>(<span class="hljs-title class_ inherited__">BaseKeychain</span>):
    <span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">get</span>(<span class="hljs-params">self, <span class="hljs-built_in">id</span>: <span class="hljs-built_in">str</span></span>) -&gt; Key | <span class="hljs-literal">None</span>:
        ...

    <span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">add</span>(<span class="hljs-params">self, key: Key</span>) -&gt; <span class="hljs-literal">None</span>:
        ...

    <span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">remove</span>(<span class="hljs-params">self, <span class="hljs-built_in">id</span>: <span class="hljs-built_in">str</span></span>) -&gt; <span class="hljs-literal">None</span>:
        ...

    <span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">list</span>(<span class="hljs-params">self</span>) -&gt; <span class="hljs-built_in">list</span>[Key]:
        ...

    <span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">current</span>(<span class="hljs-params">self</span>) -&gt; Key:
        ...

    <span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">set_current</span>(<span class="hljs-params">self, <span class="hljs-built_in">id</span>: <span class="hljs-built_in">str</span></span>) -&gt; <span class="hljs-literal">None</span>:
        ...
</code></pre>
<p>После этого реализацию можно зарегистрировать в приложении и использовать в конфигурации точно так же, как встроенные <code>FileStorage</code> и <code>MemoryStorage</code>.</p>
<p>В результате Jam ничего не знает о том, где физически лежат ключи. Они могут храниться на диске, в базе данных, во внешнем секретном хранилище или в облачном KMS. Всё взаимодействие происходит через единый интерфейс keychain.</p>
<p>Самое приятное в таком подходе - то, что PASETO, JWT, сессионные токены и любые другие механизмы внутри Jam используют один и тот же keychain. Ротация ключей, управление жизненным циклом и история ключей настраиваются один раз и затем работают одинаково для всех типов токенов.</p>
<p>Пока это только первая версия функциональности, но мне кажется странным, что подобного механизма до сих пор нет во многих БОЛЬШИХ библиотека, как authx, flask-login, fastapi-users, django-allauth и прочих. Для работы с базами данных у нас есть ORM, для кэшей - готовые абстракции и драйверы, а управление криптографическими ключами зачастую до сих пор сводится к файлу в директории проекта и нескольким строчкам самописного кода. У нас есть JWKS-клиенты, но там все равно все ложится на плечи разработчика и происходит дублирование механизмов из проекта в проект.</p>
<p>Надеюсь, что в Jam этот пробел получится закрыть.</p>]]></content:encoded>
      <pubDate>Mon, 07 Sep 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <category>Python</category>
      <category>Devlog</category>
    </item>
    <item>
      <title><![CDATA[Корпоративная LLM своими руками: vLLM, OpenWebUI и немного костылей]]></title>
      <link>https://makridenko.com/posts/2026/09/01/iternal-llm</link>
      <guid>https://makridenko.com/posts/2026/09/01/iternal-llm</guid>
      <description><![CDATA[Как я разворачивал внутреннюю LLM на H100, боролся с MIG, кешированием, контекстным окном и постепенно превращал личную игрушку в корпоративный сервис.]]></description>
      <content:encoded><![CDATA[<p>Так получилось, что в компании, где я сейчас работаю, до моего прихода не то что не использовали AI / агентов в разработке, а большинство разработчиков даже ни разу в глаза не видели, что такое этот ваш ЫЫ и с чем его едят. Кровавый энтерпрайз.</p>
<p>В итоге я стал инициатором и катализатором внедрения AI в разработку.</p>
<blockquote>
<p>Поэтому решил рассказать про свой опыт и показать, как и что я делал, чтобы это можно было повторить и вам.</p>
</blockquote>
<p>Из-за того, что это всё ещё кровавый энтерпрайз, встал вопрос о разворачивании LLM локально, внутри контура. Claude, Codex и прочие облачные решения были под тотальным запретом.</p>
<hr/>
<h1>Что у меня было</h1>
<p>После нескольких месяцев, как я ходил и всем говорил про ИИ, мне выдали машинку с Nvidia H100, и я пошёл разбираться, как всё это работает. До этого у меня был только опыт запуска моделей на домашнем компьютере, поэтому изначально казалось, что всё примерно одинаково. Но довольно быстро выяснилось, что корпоративная инфраструктура умеет подкидывать свои сюрпризы.</p>
<p>На моей карточке было всего 80 ГБ видеопамяти. По меркам современных моделей это не так уж много, поэтому выбирать приходилось осторожно.</p>
<p>Я рассматривал:</p>
<ul>
<li>DeepSeek</li>
<li>Qwen3</li>
<li>Qwen3.6</li>
<li>Qwen3-Coder</li>
</ul>
<p>После небольших тестов остановился на <code>Qwen/Qwen3-Coder-30B-A3B-Instruct</code>, потому что <code>deepseek-ai/deepseek-coder-33b-base</code> (единственный более-менее адекватный DeepSeek, который помещался в память) оказался откровенно слабоват, а <code>Qwen3.6</code> вообще не завёлся из-за драйверов.</p>
<p>Но об этом чуть позже.</p>
<hr/>
<h1>Первый запуск vLLM</h1>
<p>Как и любой нормальный человек, я сначала просто поднял модель через vLLM и начал дёргать её напрямую через API.</p>
<pre><code class="hljs language-bash"><span class="hljs-built_in">mkdir</span> vllm &amp;&amp; <span class="hljs-built_in">cd</span> vllm

pip install --upgrade pip
pip install uv

uv venv --python 3.12 --seed --managed-python
<span class="hljs-built_in">source</span> ~/vllm/.venv/bin/activate

uv pip install -U \
  <span class="hljs-string">&quot;transformers&lt;5&quot;</span> \
  <span class="hljs-string">&quot;vllm==0.10.2&quot;</span>

uv run vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct \
  --host 0.0.0.0 \
  --port 8000
</code></pre>
<p>И вот тут уже начались приключения.</p>
<h1>Когда корпоративная инфраструктура встречается с LLM</h1>
<p>Некоторые уже заметили, что версии <code>vllm</code> и <code>transformers</code> здесь далеко не самые свежие. Это не случайно. Карту мне выдали через MIG, а вместе с ней достался целый набор ограничений. Основная проблема была в драйверах. Обновить их я не мог, а из-за архитектурных и бюрократических особенностей администраторы тоже не смогли этого сделать.</p>
<p>Именно поэтому у меня так и не завёлся Qwen 3.6.</p>
<p>Но на этом веселье не закончилось.</p>
<p>Каждую ночь примерно в 2:00 MIG на несколько секунд исчезал из системы, а потом возвращался уже с новым UID. Разбираться, кто именно виноват, желания не было, поэтому я принял максимально инженерное решение:</p>
<p>Каждое утро в 5:00 сервер просто перезагружается.</p>
<p>В результате:</p>
<ul>
<li>появляется свежий UID;</li>
<li>исчезают накопившиеся проблемы;</li>
<li>получается профилактическая перезагрузка перед началом рабочего дня.</li>
</ul>
<p>Да, костыль, но зато рабочий.</p>
<p><img src="https://makridenko.com/imgs/llm/flow-1.svg" alt="flow-1"/></p>
<h1>Почему одного vLLM недостаточно</h1>
<p>Несколько дней я спокойно пользовался моделью сам. Но довольно быстро стало понятно, что такой подход подходит только для одного человека. Если моделью начнут пользоваться другие сотрудники, понадобится:</p>
<ul>
<li>веб-интерфейс;</li>
<li>управление пользователями;</li>
<li>интеграция с корпоративной авторизацией;</li>
<li>возможность быстро отзывать доступы;</li>
<li>OpenSource-решение, которое можно дорабатывать самостоятельно.</li>
</ul>
<p>После небольшого исследования я остановился на <a href="https://openwebui.com">OpenWebUI</a>. Там оказалось всё, что мне было нужно, и даже больше.</p>
<h1>OpenWebUI как точка входа</h1>
<p>Сначала OpenWebUI жил на той же машине, что и vLLM. Но довольно быстро стало заметно, что он начинает отъедать ресурсы, которые хотелось бы оставить модели. Поэтому я поднял отдельную виртуальную машину. В итоге схема стала такой:</p>
<p>Пользователь работает с OpenWebUI, а уже OpenWebUI отправляет запросы в vLLM.
Это оказалось удобно сразу по нескольким причинам:</p>
<ul>
<li>можно ограничивать доступ отдельным пользователям или группам;</li>
<li>можно собирать статистику;</li>
<li>можно собирать оценки ответов;</li>
<li>можно централизованно управлять системными промптами.</li>
</ul>
<p><img src="https://makridenko.com/imgs/llm/flow-2.svg" alt="flow-2"/></p>
<h1>Выжимаем максимум из 80 ГБ памяти</h1>
<p>После этого я начал экспериментировать с настройками модели, первой мыслью было увеличить контекстное окно.
Попробовал так:</p>
<pre><code class="hljs language-bash">uv run vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct \
  --max-model-len 120000
</code></pre>
<p>Не взлетело.</p>
<p>После серии экспериментов пришёл к такой конфигурации:</p>
<pre><code class="hljs language-bash">uv run vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct \
  --host 0.0.0.0 \
  --port 8000 \
  --dtype bfloat16 \
  --max-model-len 70000 \
  --gpu-memory-utilization 0.82 \
  --max-num-seqs 8 \
  --max-num-batched-tokens 8192 \
  --enable-auto-tool-choice \
  --tool-call-parser qwen3_coder \
  --served-model-name Qwen-Coder \
  --enable-prefix-caching \
  --enable-chunked-prefill \
  --kv-cache-dtype fp8
</code></pre>
<p>Это позволило получить примерно 70 тысяч токенов контекста. Правда, довольно быстро выяснилось, что и этого мало.</p>
<h1>Костыль 1: восстанавливаем кеш после перезагрузки</h1>
<p>Самое очевидное решение - использовать <strong>prefix caching</strong>. Проблема была в том, что каждое утро сервер перезагружался, а вместе с ним исчезал и весь кеш.</p>
<p>Поэтому появился ещё один костыль. После запуска системы сервис автоматически отправляет модели заранее подготовленный набор популярных запросов.</p>
<pre><code class="hljs language-python"><span class="hljs-comment"># -*- coding: utf-8 -*-</span>
<span class="hljs-comment"># /opt/openwebui/scripts/token_recache_service.py</span>

RECACHE_PROMPT: <span class="hljs-built_in">str</span> = load_recache_data()

client.chat.completions.create(
    model=<span class="hljs-string">&quot;qwen&quot;</span>,
    messages=[
        {<span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;system&quot;</span>, <span class="hljs-string">&quot;content&quot;</span>: RECACHE_PROMPT},
        {<span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;user&quot;</span>, <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;warmup&quot;</span>}
    ]
)
</code></pre>
<p>Да, по сути я просто заставляю модель заново посчитать нужные токены. Список популярных запросов собирался совместно с агентом и постепенно пополняется.</p>
<p>Пока такого решения хватает.</p>
<p><img src="https://makridenko.com/imgs/llm/flow-3.svg" alt="flow-3"/></p>
<h1>Оборачиваем всё в сервис</h1>
<p>Дальше уже пошла привычная инфраструктурная работа. Поднял <strong>PostgreSQL</strong> вместо <strong>SQLite</strong>, настроил <strong>nginx</strong>, выписал сертификаты и максимально закрыл доступ к серверу с <strong>vLLM</strong>.</p>
<p>К модели может обращаться только <strong>OpenWebUI</strong>.</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">postgres:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:17</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">open-webui-postgres</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>

  <span class="hljs-attr">open-webui:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/open-webui/open-webui:main</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">open-webui</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
</code></pre>
<p>Это сразу решило несколько задач.</p>
<ul>
<li>Во-первых, никто больше не может напрямую ходить в модель.</li>
<li>Во-вторых, если мне нужно провести тесты или обслуживание, я просто отключаю модель в OpenWebUI.</li>
<li>В-третьих, пользоваться системой теперь могут не только разработчики через VSCode, Zed или Opencode, но и любые другие сотрудники через обычный браузер.</li>
</ul>
<p><img src="https://makridenko.com/imgs/llm/schema-2.svg" alt="schema-2"/></p>
<h1>Костыль 2: модель тоже хочет кофе</h1>
<p>Через некоторое время я заметил интересную закономерность. Если модель долго никто не использовал, первые несколько запросов утром выполнялись заметно хуже. Она дольше думала, чаще галлюцинировала и в целом вела себя странно. После нескольких запросов всё приходило в норму.Сначала я решил, что видеокарта просто простаивает и &quot;остывает&quot;.</p>
<p>Погуглил, убедился, что подобные наблюдения встречаются не только у меня, и написал простой скрипт для минимальной нагрузки GPU.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> torch

DEVICE = <span class="hljs-string">&quot;cuda&quot;</span>

a = torch.randn((<span class="hljs-number">2048</span>, <span class="hljs-number">2048</span>), device=DEVICE, dtype=torch.float16)
b = torch.randn((<span class="hljs-number">2048</span>, <span class="hljs-number">2048</span>), device=DEVICE, dtype=torch.float16)

<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    c = torch.matmul(a, b)
    torch.cuda.synchronize()
    time.sleep(<span class="hljs-number">10</span>)
</code></pre>
<p>Работало.</p>
<p>Но было ощущение, что я просто зря трачу ресурсы.</p>
<p>Поэтому позже заменил его на другой вариант.</p>
<p>Теперь раз в несколько минут отправляется бессмысленный запрос к модели:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> requests

URL = os.getenv(<span class="hljs-string">&quot;LLM_API&quot;</span>)
TIMEOUT = <span class="hljs-number">300</span>

<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
    <span class="hljs-keyword">try</span>:
        requests.post(
            URL,
            json={
                <span class="hljs-string">&quot;model&quot;</span>: <span class="hljs-string">&quot;qwen&quot;</span>,
                <span class="hljs-string">&quot;messages&quot;</span>: [
                    {<span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;user&quot;</span>, <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;ping&quot;</span>}
                ],
                <span class="hljs-string">&quot;max_tokens&quot;</span>: <span class="hljs-number">1</span>,
            },
            timeout=<span class="hljs-number">30</span>,
        )
    <span class="hljs-keyword">except</span> Exception:
        <span class="hljs-keyword">pass</span>

    time.sleep(TIMEOUT)
</code></pre>
<p>В результате модель никогда не простаивает слишком долго, а первые реальные запросы пользователей отрабатывают заметно стабильнее.</p>
<p><img src="https://makridenko.com/imgs/llm/flow-4.svg" alt="flow-4"/></p>
<h1>Финальная схема</h1>
<p>Последним штрихом мы вместе с девопсом настроили вход через FreeIPA, выписали внутренний сертификат и добавили сервис в корпоративный DNS.</p>
<p>В итоге получилась такая архитектура:</p>
<p><img src="https://makridenko.com/imgs/llm/scheme.svg" alt="scheme"/></p>
<h1>Что дальше?</h1>
<p>Сейчас система уже активно используется внутри компании и постепенно обрастает дополнительными возможностями. Следующим шагом хочу вынести проектные знания в отдельный RAG-сервис и отдельно заняться долговременным хранением и восстановлением кешей. И в ближайшее время у меня появится машинка на H200, где я буду тестировать более умные модели.</p>
<p>Но это уже история для следующей статьи.</p>]]></content:encoded>
      <pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <enclosure url="https://makridenko.com/imgs/llm/flow-1.svg" type="image/svg+xml" length="0"/>
      <category>AI</category>
      <category>Tutorial</category>
    </item>
    <item>
      <title><![CDATA[Delta by Zed]]></title>
      <link>https://makridenko.com/posts/2026/08/30/delta-by-zed</link>
      <guid>https://makridenko.com/posts/2026/08/30/delta-by-zed</guid>
      <description><![CDATA[Я попал в whitelist Delta и немного попользовался им по рабочим и не очень рабочим делам. И мне очень понравилось - это именно то, чего мне не хватало. Это не уродское консольное нечто, написанное на JS и React, не кривой обрезанный экстеншен для Zed или VS Code. Это нативное быстрое приложение, которое много что умеет прямо из коробки.]]></description>
      <content:encoded><![CDATA[<p><img src="https://makridenko.com/imgs/delta-by-zed/delta-invite-mail.png" alt="delta-invite-mail"/></p>
<p>Я тут попал в whitelist <a href="https://delta.dev">Delta</a> и немного попользовался им по рабочим и не очень рабочим делам. И мне очень понравилось - это именно то, чего мне не хватало. Это не уродское консольное нечто, написанное на JS и React, не кривой обрезанный экстеншен для Zed или VS Code. Это <strong>нативное</strong> быстрое приложение, которое много что умеет прямо из коробки.</p>
<p><img src="https://makridenko.com/imgs/delta-by-zed/delta-ui.png" alt="ui"/></p>
<h2>Что там есть такого, чего нет в Claude Code, Codex и OpenCode?</h2>
<h3>Во-первых, очень крутое ревью</h3>
<p><img src="https://makridenko.com/imgs/delta-by-zed/delta-review.png" alt="review"/></p>
<p>До этого я пользовался <a href="https://revdiff.com">revdiff</a> от <a href="https://github.com/umputun">Umputun&#x27;а</a>, но, объективно, это прям костыль-костыль. Открывать через tmux и терминальные оверлеи какие-то TUI-программы, которые потом при выходе просто пишут <code>номер_строки: комментарий</code>, - ну такое себе.</p>
<p>Тут же ты ходишь по коду, оставляешь комментарии и потом одним действием отправляешь их агенту. Очень похоже на то, как мы делаем ревью в GitLab или GitHub. Очень круто.</p>
<h3>Во-вторых, контроль работы через worktree</h3>
<p>Чуваки из <a href="https://atom-editor.cc/">Atom</a>,  а именно они пилят Zed и Delta, давно топят за работу через Git worktree, ещё со времён Atom.
Мне этот подход тоже нравится. Особенно во времена агентного кодинга не очень хочется пускать агента в рабочее дерево напрямую. Гораздо удобнее создать worktree от текущего состояния проекта и пусть агент там развлекается. После ревью изменения можно перенести в основную ветку.</p>
<p>Вот <strong>Delta</strong> так и делает: для каждого треда создаётся отдельный worktree, в котором происходит вся работа. Это и безопасно, и удобно, особенно когда параллельно работают несколько агентов.</p>
<p><img src="https://makridenko.com/imgs/delta-by-zed/delta-tree.png" alt="tree-pic"/></p>
<h3>В-третьих, довольно узкий кейс, но...</h3>
<p>Совместная работа.</p>
<p>Я могу расшарить сессию и вместе с коллегой работать над задачей в реальном времени. Мне <strong>очень</strong> нравится эта фича в самом Zed. На работе я так периодически смотрю код с коллегами, и это гораздо удобнее, чем шарить экран в созвоне и пытаться что-то показывать.
А когда работа из написания кода превратилась в погоню за ИИ-агентами, логично стало шарить уже не код, а сессию: с планами, рассуждениями и тем же самым кодом, но ещё и с diff&#x27;ами от агента.</p>
<p>Это мало кому нужно, но лично я считаю это одной из лучших фич как в Zed, так и в Delta.</p>
<h3>В-четвёртых, НАТИВНОСТЬ</h3>
<p>ДА БОЖЕ ДА!</p>
<p>Как же я устал от всего этого зоопарка на Electron, JS и TS. Зачем? Для чего?
(Иронично, что именно создатели Zed когда-то создали Electron для Atom, а теперь стараются всё делать нативно. Замаливают грехи на 100%.)
Вот зачем, например, Bitwarden писать на TypeScript? И десктопное приложение, и CLI. Зачем Claude Code писать на TypeScript? Да ещё и с React?</p>
<p>Electron в целом стал настоящим бичом современного десктопа, поэтому то, что Delta быстрая и нативная, - огромный плюс.</p>
<hr/>
<h3>А чего реально не хватает?</h3>
<h4>В первую очередь, конечно, кастомных провайдеров.</h4>
<p>Очень хочется подключить рабочую модель со своими токенами (в первую очередь, чтобы ИБ-шники не настучали по голове) и работать через неё. Возможно, этого пока нет просто потому, что Delta всё ещё находится в закрытой бете. Но в роудмапе я не увидел ни слова про кастомных провайдеров.</p>
<h4>Потом, конечно, работа с Git.</h4>
<p>Сейчас предлагают переносить изменения через <code>stash</code> или <code>checkout</code> коммитов из worktree в своём терминале вне Delta. В целом я не против, но хотелось бы делать это немного удобнее. К счастью, в роудмапе это уже есть, так что, скорее всего, допилят.</p>
<h4>Ну и переключение агентов.</h4>
<p>Хочется сначала построить план с read-only агентом, а потом уже отправить другого агента выполнять работу. Пока такой возможности нет, но посмотрим, как будет развиваться продукт.</p>
<hr/>
<p>В какой-то момент работы я поймал себя на мысли, что воспринимаю delta не как очередной инструмент для работы с <strong>ЫЫ</strong> и даже не как &quot;агентскую IDE&quot;. С появлением полноценных агентов в нашей работе сам процесс разработки поменялся. Если раньше редактор кода был местом, где ты в основном писал код сам, то сейчас редактор кода открывается лишь для того, чтобы прочитать код за агентом и то очень редко, дифов в самом клод-коде / кодексе часто хватает. В delta все это есть, тут можно и прочитать весь код проекта, и если надо, руками его поредактировать, оставить комментарии для робота даже вне диффов.</p>
<p>Именно поэтому delta ощущается не как ещё один чатик, прикрученный к редактору, а как попытка построить редактор кода следующего поколения. Здесь агент становится не дополнением к привычному процессу, а одной из основных сущностей системы наряду с кодом (или даже выше), Git и разработчиком.</p>]]></content:encoded>
      <pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <enclosure url="https://makridenko.com/imgs/delta-by-zed/delta-ui.png" type="image/png" length="0"/>
      <category>AI</category>
      <category>Tools</category>
    </item>
    <item>
      <title><![CDATA[#курилка]]></title>
      <link>https://makridenko.com/posts/2026/07/21/kurilka-podcast-tiser</link>
      <guid>https://makridenko.com/posts/2026/07/21/kurilka-podcast-tiser</guid>
      <description><![CDATA[Анонс нашего подкаста "#курилка"]]></description>
      <content:encoded><![CDATA[<p>Я окончательно превращаюсь в медиа-клопа и представляю вам совместный с <a href="https://github.com/ShIIIrochka">Ксенией</a> подкаст - <strong>#курилка</strong>.</p>
<hr/>
<h2>Кто? Что? И зачем?</h2>
<p>Я с Ксюшей часто хожу на митапы, конференции и IT-сходки, и там мы очень любим холиварить. Вот я и подумал: а почему бы не вынести наши холивары, интересные (и не очень) темы в формат <code>публичный_формат.mp3</code>?</p>
<p>Будем обсуждать темы, которые нам нравятся и волнуют нас. Может быть, актуальные, может быть — не очень. Какой-то закреплённой тематики не будет, потому что потому.</p>
<p>В пилотном выпуске поговорим про &quot;карательную литературу&quot;: Clean Code, DDD, паттерны и всё остальное, что портит людей.</p>
<h2>А когда?</h2>
<p>Ээээээ... не знаю. В максимально ближайшее время. Неделя, две, три. Возможно, по законам всех IT-проектов сроки ещё будут пересмотрены. Статус по задаче актуализирую, туда сюда</p>
<h2>Где?</h2>
<p>На популярных площадках для подкастов: Spotify, Apple Podcasts, Яндекс Музыка, YouTube, Pocket Casts и так далее.
Если всё пойдёт по плану - там и появимся. Если не по плану - тоже появимся, просто позже.</p>]]></content:encoded>
      <pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <enclosure url="https://makridenko.com/imgs/kurilka-cover.png" type="image/png" length="0"/>
      <category>Podcast</category>
    </item>
    <item>
      <title><![CDATA[Мысли про Jam]]></title>
      <link>https://makridenko.com/posts/2026/05/25/some-think-about-jam</link>
      <guid>https://makridenko.com/posts/2026/05/25/some-think-about-jam</guid>
      <description><![CDATA[Мне не нравится, как сейчас устроен главный инстанс jam.*.Jam. Сейчас это, по сути, ненужная надстройка над основными модулями. Поэтому я хочу переделать полностью концепцию главного инстанса, но оставить и немного расширить модули.]]></description>
      <content:encoded><![CDATA[<h2>Небольшой дисклеймер</h2>
<p>Jam — это чисто проект для души, я делаю его в первую очередь для себя. Поэтому многие решения принимаются просто потому что мне так хочется.</p>
<hr/>
<p>Мне не нравится, как сейчас устроен главный инстанс <code>jam.*.Jam</code>. Сейчас это, по сути, ненужная надстройка над основными модулями. Например, PASETO можно сгенерировать двумя способами:</p>
<pre><code class="hljs language-python"><span class="hljs-comment"># через инстанс</span>
<span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam

config = {
    <span class="hljs-string">&quot;paseto&quot;</span>: {
        <span class="hljs-string">&quot;version&quot;</span>: <span class="hljs-string">&quot;v4&quot;</span>,
        <span class="hljs-string">&quot;purpose&quot;</span>: <span class="hljs-string">&quot;local&quot;</span>,
        <span class="hljs-string">&quot;secret_key&quot;</span>: <span class="hljs-string">&quot;SOME_SECRET&quot;</span>
    }
}

jam = Jam(config=config)
paseto = jam.paseto_create(payload={<span class="hljs-string">&quot;some&quot;</span>: <span class="hljs-string">&quot;body&quot;</span>})

<span class="hljs-comment"># ---</span>
<span class="hljs-comment"># и через модуль</span>
<span class="hljs-keyword">from</span> jam.paseto <span class="hljs-keyword">import</span> PASETOv4

paseto = PASETOv4.key(
    purpose=<span class="hljs-string">&quot;local&quot;</span>,
    secret_key=<span class="hljs-string">&quot;SOME_SECRET&quot;</span>
).encode(payload={<span class="hljs-string">&quot;some&quot;</span>: <span class="hljs-string">&quot;body&quot;</span>})
</code></pre>
<p>И мне это не нравится. Сейчас нет ни одной реальной причины использовать инстанс, кроме удобного описания конфига в YAML (или любом другом формате).</p>
<p>Поэтому я думаю добавить в каждый инстанс (в <code>__init__</code>) два новых аргумента: <code>config: str | dict | None</code> и <code>pointer: str | None</code> — то есть примерно как в <code>jam.*.Jam</code>. Тогда можно будет делать что-то вроде:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">class</span> <span class="hljs-title class_">JWT</span>(<span class="hljs-title class_ inherited__">BaseJWT</span>):
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">
        self,
        config: <span class="hljs-built_in">str</span> | <span class="hljs-built_in">dict</span>[<span class="hljs-built_in">str</span>, <span class="hljs-type">Any</span>],
        pointer: <span class="hljs-built_in">str</span> = <span class="hljs-string">&quot;jam&quot;</span>,
        *,
        alg: <span class="hljs-built_in">str</span> | <span class="hljs-literal">None</span> = <span class="hljs-literal">None</span>,
        enc: <span class="hljs-built_in">str</span> | <span class="hljs-literal">None</span> = <span class="hljs-literal">None</span>,
        secret_key: <span class="hljs-built_in">str</span> | <span class="hljs-built_in">bytes</span> | KeyLike | <span class="hljs-string">&quot;JWK&quot;</span> | <span class="hljs-literal">None</span> = <span class="hljs-literal">None</span>,
        password: <span class="hljs-built_in">str</span> | <span class="hljs-built_in">bytes</span> | <span class="hljs-literal">None</span> = <span class="hljs-literal">None</span>,
        <span class="hljs-built_in">list</span>: <span class="hljs-built_in">dict</span>[<span class="hljs-built_in">str</span>, <span class="hljs-type">Any</span>] | <span class="hljs-literal">None</span> = <span class="hljs-literal">None</span>,
        serializer: BaseEncoder | <span class="hljs-built_in">type</span>[BaseEncoder] = JsonEncoder,
        logger: BaseLogger = logger,
        jws: JWS | <span class="hljs-literal">None</span> = <span class="hljs-literal">None</span>,
        jwe: JWE | <span class="hljs-literal">None</span> = <span class="hljs-literal">None</span>,
    </span>) -&gt; <span class="hljs-literal">None</span>: ...
</code></pre>
<p>То есть можно будет передавать как конфиг, так и отдельные параметры — кому как удобнее, и не дергать <code>jam.*.Jam</code>.</p>
<hr/>
<h2>Ну а что делать с <code>jam.*.Jam</code>?</h2>
<p>Полностью вырезать класс я точно не буду, но сама концепция сильно изменится.</p>
<p>Пока не могу точно сказать, каким он станет в итоге, но хочу сделать механизм для описания <code>Subject</code>&#x27;а (пользователя или любого другого объекта) и его авторизации.</p>
<p>Скорее всего, это будет выглядеть примерно так:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam <span class="hljs-keyword">import</span> Jam, BaseSubject

<span class="hljs-comment"># всё ниже чисто набросок по наитию, по вайбу</span>
<span class="hljs-keyword">class</span> <span class="hljs-title class_">User</span>(<span class="hljs-title class_ inherited__">BaseSubject</span>):
    <span class="hljs-built_in">id</span>: UUID
    email: <span class="hljs-built_in">str</span>

<span class="hljs-keyword">class</span> <span class="hljs-title class_">MyAuthorization</span>(<span class="hljs-title class_ inherited__">Jam</span>):
    subject = User
    config = <span class="hljs-string">&quot;config.toml&quot;</span>

<span class="hljs-comment"># ---</span>
<span class="hljs-comment"># а тут уже начинается магия</span>

auth = MyAuthorization()

user = User(<span class="hljs-built_in">id</span>=uuid4(), email=<span class="hljs-string">&quot;someuser@mail.com&quot;</span>)

auth.authorize(subject=user)
</code></pre>
<p>Наверное, что-то в таком духе. Пока всё это только обдумываю и проверяю разные идеи.</p>
<hr/>
<p>Сейчас я делаю для Jam механизмы <strong>SAML</strong> и не буду встраивать их в <code>jam.*.Jam</code>. SAML с самого начала будет существовать только как отдельный модуль.</p>]]></content:encoded>
      <pubDate>Mon, 25 May 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <category>Random</category>
      <category>Jam</category>
    </item>
    <item>
      <title><![CDATA[Jam 3.2.0]]></title>
      <link>https://makridenko.com/posts/2026/05/19/jam320</link>
      <guid>https://makridenko.com/posts/2026/05/19/jam320</guid>
      <description><![CDATA[Сегодня вышел Jam 3.2.0 - Наконец-то добил до полной jose спецификации.]]></description>
      <content:encoded><![CDATA[<p>Сегодня вышел <strong>Jam 3.2.0</strong> - Наконец-то добил до полной jose спецификации.</p>
<h2>Что нового</h2>
<h3>Полный JOSE-стек (RFC 7515–7519)</h3>
<p>Добавлен модуль <code>jam.jose</code> с четырьмя компонентами:</p>
<ul>
<li><strong>JWS</strong> — JSON Web Signature (RFC 7515), подпись данных с валидацией критических заголовков (<code>crit</code>)</li>
<li><strong>JWE</strong> — JSON Web Encryption (RFC 7516), шифрование с автоопределением алгоритма управления ключами (RSA -&gt; RSA-OAEP, EC -&gt; ECDH-ES, симметричные -&gt; A256KW/A128KW)</li>
<li><strong>JWK / JWKSet</strong> — JSON Web Key (RFC 7517), работа с ключами</li>
<li><strong>JWT</strong> — JSON Web Token (RFC 7519), теперь через JOSE</li>
</ul>
<p>Вложенный JWT (sign-then-encrypt) теперь работает по RFC 7519 с HKDF-выведением ключей.</p>
<h3>Новые параметры <code>jwt_decode</code> в инстанс</h3>
<ul>
<li><code>check_nbf</code> — валидация <code>nbf</code> (not-before) claim</li>
<li><code>include_headers</code> — возврат заголовков вместе с payload</li>
</ul>
<h3>Безопасность</h3>
<p>Алгоритм <code>none</code> теперь явно отключён. Тут решил нарушить спеку и жестко запретить, меньшее из зол.</p>
<p><a href="https://github.com/mkrdnk/jam/releases/tag/v3.2.0">GitHub Release</a> | <a href="https://jam.makridenko.ru">jam.makridenko.ru</a></p>]]></content:encoded>
      <pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <category>Jam</category>
      <category>Devlog</category>
      <category>Python</category>
    </item>
    <item>
      <title><![CDATA[Токены авторизации: почему JWT легко использовать неправильно и как это исправляет PASETO?]]></title>
      <link>https://makridenko.com/posts/2026/04/20/moscow-python-auth-tokens</link>
      <guid>https://makridenko.com/posts/2026/04/20/moscow-python-auth-tokens</guid>
      <description><![CDATA[В веб-разработке широко распространена аутентификация на основе токенов. Наибольшей популярностью пользуются JWT - компактные токены, несущие закодированные JSON-декларации (claims), защищённые подписью и(или) шифрованием. Несмотря на это, JWT часто применяют неправильно. Многие разработчики вставляют их в любое приложение, стремясь к stateless-аутентификации(а на деле просто не зная что брать).]]></description>
      <content:encoded><![CDATA[<blockquote>
<p>Это текстовая версия моего доклада на Moscow Python №110</p>
</blockquote>
<p>В веб-разработке широко распространена аутентификация на основе токенов. Наибольшей популярностью пользуются <strong>JWT</strong> - компактные токены, несущие закодированные JSON-декларации (claims), защищённые подписью и(или) шифрованием. JWT входят в семейство стандартов <strong>JOSE</strong> (JSON Object Signing and Encryption), которое включает механизмы JWS (подпись), JWE (шифрование), JWK (формат ключей), JWA (алгоритмы) и собственно JWT (токены).</p>
<p>Несмотря на это, JWT часто применяют неправильно. Многие разработчики вставляют их в любое приложение, стремясь к stateless-аутентификации(а на деле просто не зная что брать). Однако JWT не являются универсальным решением для всех задач авторизации. В данном докладе разберём JWT/JOSE и их недостатки, а затем посмотрим PASETO - более безопасную альтернативу с жёстким набором алгоритмов.</p>
<h2>Что такое JWT и JOSE</h2>
<p><strong>JWT (JSON Web Token)</strong> - это компактный URL-безопасный токен, предназначенный для передачи набора утверждений (claims) между сторонами. Токен состоит из трёх частей (заголовок, тело, подпись), каждая кодируется base64url и разделяется точками. Заголовок содержит параметры подписи (алгоритм, тип), тело - JSON-объект с утверждениями (например, <code>sub</code>, <code>iat</code>, <code>exp</code> и др.), а третий фрагмент - подпись (или пустая строка для несекурного режима). JWT стандартизован в RFC 7519.</p>
<p><strong>JOSE (JSON Object Signing and Encryption)</strong> - это совокупность стандартов <a href="https://www.ietf.org/">IETF</a> для криптографической защиты JSON-данных. JOSE включает: JWS - формат подписанных сообщений, JWE - шифрованных, JWK – описание ключей, JWA - набор криптоалгоритмов, а JWT как раз описывает формат токена, который можно обернуть в JWS или JWE. Иными словами, JWT - это лишь часть семейства JOSE, предназначенная для передачи «квитанций» (claims) между сервисами.</p>
<p>В норме JWT используется так: сервер аутентификации выпускает JWT и подписывает его своим секретным ключом (HS256/HMAC или RS256/RSA), клиент сохраняет этот токен и на каждый запрос к ресурсу отправляет его (например, в HTTP-заголовке). Сторона-приёмник проверяет подпись и срок жизни токена.</p>
<p>Однако уже на этом уровне скрываются некоторые «ловушки»: избыточная гибкость, необязательность проверки и тд., что и приводит к частым ошибкам.</p>
<h2>В чем проблема JWT</h2>
<p>JWT сами по себе не небезопасны – проблема в том, что спецификация JOSE даёт широкие возможности, которые легко неправильно использовать. Ключевые недостатки и уязвимости:</p>
<ul>
<li>
<p><strong>Алгоритм <code>none</code>.</strong> Спецификация позволяет задать <code>&quot;alg&quot;:&quot;none&quot;</code>, означающий отсутствие подписи. Известная уязвимость: если сервер НЕ проверяет этот параметр или библиотека некорректно обрабатывает такой токен, злоумышленник может просто указать <code>&quot;alg&quot;:&quot;none&quot;</code> и модифицировать содержимое без подписи. Многие ранние реализации принимали такие токены как валидные. Несмотря на фиксы в обновлённых библиотеках, уязвимость всё ещё встречается в устаревшем ПО.</p>
</li>
<li>
<p><strong>Конфуз алгоритмов (key confusion).</strong> JWT поддерживает как симметричные (HS256) так и асимметричные алгоритмы (RS256, ES256). Некоторые библиотеки при верификации опираются на поле <code>alg</code> из токена и на переданный ключ без жёсткой проверки типа. Вследствие этого случается атака: если сервис ожидал, что <code>alg=RS256</code>, но он явно передаёт в функцию верификации RSA-ключ, то злоумышленник может выдать токен с <code>alg=HS256</code> и подписать его тем же открытым ключом (используя его как HMAC-секрет). Сервер же проверяет HMAC с этим секретом (который на самом деле открытый ключ) и принимает такой поддельный токен. В результате злоумышленник может подделать права в токене без знания приватного ключа. Эта атака на совмещение ключей (algorithm confusion) хорошо описана в исследованиях по безопасности JWT.</p>
</li>
<li>
<p><strong>Неправильная верификация.</strong> Самая грубая ошибка - просто не проверять подпись. Например, в JavaScript-библиотеке Node.js функция <code>jwt.decode()</code> только раскодирует тело, но не проверяет подпись. В документации OWASP отмечается, что если использовать <code>decode</code> вместо <code>verify</code>, можно читать данные токена без проверки подписи - это прямая уязвимость.</p>
</li>
<li>
<p><strong>Сложность и «дырки» в использовании.</strong> Спецификация JOSE большая и многослойная. Например, JWT поддерживает необязательные поля <code>kid</code>, <code>jku</code> (идентификатор или URL ключа) - их неверная обработка позволяет проводить атаки типа подмены ключей. Детали формата JWE (шифрование) сложны и редко корректно применяются - часто JSON Web Encryption вообще игнорируют. Массив возможностей приводит к тому, что разработчики могут невольно допустить уязвимость.</p>
</li>
<li>
<p><strong>Перенаправление токенов и отзыв.</strong> JWT изначально спроектированы как самодостаточные токены (stateless). Это означает, что верификация обычно сводится к проверке подписи и сроков. Если надо отозвать токен досрочно, приходится вводить дополнительные механизмы (чёрные списки), иначе токен жил до конца срока. Такой способ управления сеансом далеко не всегда подходит. JWT - это заявленные утверждения (claims) о субъекте. Если приложение хранит состояние в JWT, возникают сложности с отзывом прав, разлогином и т.п. Например, JWT часто применяют как сессии без остановки - нечего логицировать, токен валиден до <code>exp</code> и всё.</p>
</li>
</ul>
<p>Подводя итог: гибкость JWT даёт много возможностей, но без строгого контроля легко наломать дров. Много стандартных библиотек JWT позволяли простые обходы аутентификации (вставка <code>alg: none</code>, игнорирование подписи, конфуз алгоритмов). Более того, по данным анализа, разработчики часто применяют JWT без особой надобности - просто из-за популярности и штампов.</p>
<h2>Как и где JWT используют неправильно</h2>
<p>Типичные паттерны неправильного применения JWT:</p>
<ul>
<li>
<p><strong>JWT как сессионный токен.</strong> Многие воспринимают JWT как замену обычной серверной сессии. Однако JWT статичны: сам токен содержит все данные и верифицируется только локально по подписи. Если после выдачи токена администратор изменил права пользователя, JWT сам об этом не узнает. Это ведёт к неотзываемым сессиям, невозможности реализовать logout и тд.</p>
</li>
<li>
<p><strong>Хранение секретов в payload.</strong> Поскольку тело JWT обычно не шифруется (если использовать JWS), туда нередко кладут лишние данные. Разработчики могут запихать личные сведения или внутреннюю логику приложения в поля токена, считая его «закрытым». Но payload JWT открыт любому, у кого есть токен. Обнаружение секретных полей в чётко читаемом JSON - классика.
<img src="https://makridenko.com/imgs/auth-tokens-moscow-python/haha-classic.jpg" alt="hahaclassic"/></p>
</li>
<li>
<p><strong>Использование небезопасных алгоритмов.</strong> Иногда в качестве быстрых решений применяют устаревшие алгоритмы или слабые ключи. Например, использование <code>HS256</code> с коротким симметричным ключом вообще не обеспечивает должную защиту. Если JWT подписан по HMAC, то секретный ключ должен быть достаточно длинным и случайным. В противном случае его можно подобрать. Или берутся неподходящие ключи RSA (RSA PKCS#1v1.5), которые считаются менее безопасными. Разработчик может просто указать любой алгоритм - спецификация не мешает. Всё это увеличивает риск взлома токена.</p>
</li>
<li>
<p><strong>Неправильная обработка ошибок.</strong> Если сервер по какой-то причине не может проверить подпись (например, нет ключа для верификации), встречается практика «просто пропустить» такую ошибку и вернуть payload. Это катастрофа: любой кривой токен принимается. Версия OWASP подчёркивает важность запрета любых JWT, для которых не прошла верификация подписи.</p>
</li>
<li>
<p><strong>Отправка JWT не по HTTPS.</strong> Несмотря на общие рекомендации, практикуется отправка токена в незащищённом канале (например, убрав HTTPS). Тогда подпись токена хоть и проверяется, но токен можно перехватить. Сразу же доказано неэффективность любого криптоалгоритма, если токен протаскивается по незащищённой сети.</p>
</li>
<li>
<p><strong>Необоснованная многословность.</strong> Некоторые компоненты умудряются поддерживать JWE (шифрование), JWS (подпись), JWK (ключи) и прочее. Разработчик путается, включает в конфиг всё подряд. На практике часто достаточно либо только подписи, либо только шифрования, но не нужно одновременно клеить всё. Слишком сложная схема - это повышенный риск ошибки.</p>
</li>
</ul>
<p>В общем, наиболее частая ошибка - выбор JWT по причине: <strong>&quot;НУ А ЧТО ЕСЛИ НЕ ДЖЕ ВЕ ТЕ?!&quot;</strong>, без анализа требований безопасности. Часто проще было бы завести на сервере сессионное хранилище или выпустить короткоживущий токен, чем пихать JWT и бороться с его сложностями.</p>
<h2>Неочевидные проблемы JWT</h2>
<p>Кроме перечисленных базовых уязвимостей, существуют и менее очевидные подводные камни:</p>
<ul>
<li>
<p><strong>Шаблоны использования.</strong> JWT не предусматривает <em>обновления</em> (refresh) без выдачи нового токена, поскольку истечение (<code>exp</code>) встроено внутрь токена. Это затрудняет ротацию ключей или выдачу нового токена без потери сессии. То есть вы либо доверяете старому токену до конца срока, либо вынуждены выдавать новый, отслеживать, какие старые ещё валидны.</p>
</li>
<li>
<p><strong>Невалидные претензии (claims).</strong> JWT определяет набор зарезервированных полей (<code>iss</code>, <code>sub</code>, <code>aud</code>, <code>exp</code> и др.), но они не обязательны. Разработчики иногда по умолчанию полагаются только на <code>exp</code>, забывая проверять <code>iss</code> (издатель) и <code>aud</code> (аудитория), что позволяет смешивать токены из разных систем. Например, сервер может не проверять, что <code>iss</code> – это именно его издатель, и принять чужой JWT.</p>
</li>
<li>
<p><strong>Ключевой менеджмент.</strong> В JWT/JOSE ключи описываются через JWK (JSON Web Key) - громоздкая JSON-структура. Код, получающий токен, должен найти нужный ключ (по <code>kid</code> или URL), загрузить его и распарсить. Ошибки в этих шагах привели к атакам (например, <code>jku</code> - URL, откуда взять ключ), когда клиент мог указать свою <code>jku</code>. Сложная схема управления ключами - ещё одна точка отказа/атаки.</p>
</li>
</ul>
<pre><code class="hljs language-json"><span class="hljs-comment">// пример EC P256 ключа в формате JWK</span>
<span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;kty&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;EC&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;kid&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ec1&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;use&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sig&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;crv&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;P-256&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;alg&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ES256&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;x&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;f83OJ3D2xF4... (base64url координата x)&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;y&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;x_FEzRu9G6... (base64url координата y)&quot;</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<ul>
<li>
<p><strong>Размер токена.</strong> JWT часто хранятся в HTTP-заголовках или куки. Поскольку кодирование base64увеличивает размер, а payload обычно JSON-текст, токены могут быть довольно большими (сотни байт). Это снижает пропускную способность и может вызвать проблемы (например, превышение размера HTTP-заголовков или куки).</p>
</li>
<li>
<p><strong>Многофункциональность.</strong> JWT по стандарту может и шифроваться (JWE), и подписываться (JWS). Если начать комбинировать (подписать, затем зашифровать), то обратная сторона должна это поддерживать. На практике разработчики часто просто подписывают JWT, но формально спецификация разрешает и иные сценарии. Многозадачность системы увеличивает поверхность атаки - чем больше опций, тем больше багов.</p>
</li>
</ul>
<p>Таким образом, даже неявные атрибуты JWT (алгоритмы, header-поля, зависимости от контекста) создают риск. Как отмечено в <a href="https://github.com/paseto-standard/paseto-spec">анализе PASETO</a>, многие проблемы JWT связаны с избыточной гибкостью: разработчик выбирает алгоритмы и форматы и может ошибиться.</p>
<h2>Что такое PASETO</h2>
<p><strong>PASETO (Platform-Agnostic SEcurity TOkens)</strong> – это предложенный Paragon Initiative стандарт токенов безопасности. Главная идея: предоставить токенную замену JWT, в которой <strong>из коробки</strong> используются только безопасные, современные алгоритмы, а разработчик лишён возможности выбрать бессмысленные варианты.</p>
<p>В PASETO токен имеет формат <code>v{version}.{purpose}.{body}.{footer?}</code>, где нет отдельного поля <code>alg</code>. Вместо этого:</p>
<ul>
<li><code>version</code> (v1, v2, v3, v4) определяет криптографический протокол и алгоритмы (AES/GCM в v1–v2, XChaCha20-Poly1305 в v2–v4, EdDSA/ECDSA в v3–v4 и т.д.).</li>
<li><code>purpose</code> – режим: <code>local</code> (шифрование с симметричным ключом) или <code>public</code> (подпись с публичным ключом).</li>
</ul>
<p>Например, токен <code>v4.public.</code> означает PASETO версии 4, режим публичной подписи (Ed25519). В этом случае тело токена подписывается приватным ED25519, а любой с соответствующим публичным ключом может проверить подпись. Если <code>local</code>, то токен шифруется симметричным ключом XChaCha20-Poly1305. Фактически PASETO предоставляет два варианта: <strong>шифрование (конфиденциальность) или подпись (целостность)</strong>, как JWE/JWS, но с жёстко предопределёнными алгоритмами.</p>
<p>Ключевые отличия PASETO от JWT:</p>
<ul>
<li><strong>Безопасность по умолчанию:</strong> Нельзя выбрать <em>несесурный</em> алгоритм. <code>None-атака</code> была забытым нейтральным вариантом в JWT; в PASETO её просто нет.</li>
<li><strong>Versioned Protocols:</strong> В JWT есть &quot;алгоритмическая гибкость&quot; (можно свободно переключаться между HS256, RS256, HS512 и пр.), что порождает ошибки. PASETO вместо этого задаёт версии, где каждому набору алгоритмов соответствует своя версия. Это исключает двусмысленность.</li>
<li><strong>Простота:</strong> PASETO убирает все нестандартные расширения JOSE. Нет <code>kid/jku/jwk</code>, нет разных типов контента – только JSON-поледательность и подпись/шифрование. Спецификация просто чище.</li>
<li><strong>Прозрачность:</strong> Структура токена очевидна по префиксу (v2/local или v4/public), не требуется дополнительная информация о ключах.</li>
</ul>
<p>Важно понимать: ни JWT, ни PASETO не сами по себе не решают проблему <strong>без состояний</strong>. PASETO также не предусматривает механизмов предотвращения replay-атак – для этого всё равно нужны серверные слежения. Однако по безопасности PASETO зарекомендовал себя как способ не дать разработчику прострелить ногу. В спецификации PASETO прямо сказано, что задачи stateless-сессий лежат вне его ответственности; он просто гарантирует криптостойкость при передаче данных.</p>
<h2>Как устроен PASETO (с примерами, jam.paseto)</h2>
<p>Рассмотрим внутренности PASETO на примере версий v2 (XChaCha20+Ed25519, устаревшая) и v4 (libsodium-актуальные алгоритмы) на примере моей библиотеки <a href="https://jam.makridenko.ru/">Jam</a>.</p>
<blockquote>
<p>там кстати не только PASETO, там и JWT, и OTP, и серверные сессии, и OAuth2. Приходите ставьте звездочку все дела :)</p>
</blockquote>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> jam.paseto.v4 <span class="hljs-keyword">import</span> PASETOv4
<span class="hljs-keyword">from</span> cryptography.hazmat.primitives.asymmetric.ed25519 <span class="hljs-keyword">import</span> Ed25519PrivateKey
<span class="hljs-keyword">import</span> secrets

<span class="hljs-comment"># Симметрический режим (local)</span>
<span class="hljs-comment"># Генерируем 32-байтовый секрет</span>
secret_key = secrets.token_bytes(<span class="hljs-number">32</span>)
<span class="hljs-comment"># Создаём PASETO-инстанс для шифрования (v4.local)</span>
paseto_local = PASETOv4.key(<span class="hljs-string">&quot;local&quot;</span>, secret_key)

<span class="hljs-comment"># Кодируем токен с JSON-платёжкой и необязательным футером (footer)</span>
token_local = paseto_local.encode(
    {<span class="hljs-string">&quot;user&quot;</span>: <span class="hljs-string">&quot;meowl&quot;</span>, <span class="hljs-string">&quot;exp&quot;</span>: <span class="hljs-string">&quot;2030-01-01T00:00:00+00:00&quot;</span>},
    footer=<span class="hljs-string">&quot;service-identifier&quot;</span>
)
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;PASETO local:&quot;</span>, token_local)

<span class="hljs-comment"># Раскодируем и проверим токен</span>
payload, footer = paseto_local.decode(token_local)
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Payload:&quot;</span>, payload)   <span class="hljs-comment"># {&#x27;user&#x27;: &#x27;meowl&#x27;, &#x27;exp&#x27;: &#x27;2030-01-01T00:00:00+00:00&#x27;}</span>
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Footer:&quot;</span>, footer)     <span class="hljs-comment"># &#x27;service-identifier&#x27;</span>

<span class="hljs-comment"># Асимметрический режим (public)</span>
<span class="hljs-comment"># Генерируем ED25519-ключи</span>
private_key = Ed25519PrivateKey.generate()
public_paseto = PASETOv4.key(<span class="hljs-string">&quot;public&quot;</span>, private_key)

token_public = public_paseto.encode(
    {<span class="hljs-string">&quot;user&quot;</span>: <span class="hljs-string">&quot;mewfish&quot;</span>, <span class="hljs-string">&quot;exp&quot;</span>: <span class="hljs-string">&quot;2030-01-01T00:00:00+00:00&quot;</span>}
)
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;PASETO public:&quot;</span>, token_public)

payload_pub, _ = public_paseto.decode(token_public)
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Payload:&quot;</span>, payload_pub)  <span class="hljs-comment"># {&#x27;user&#x27;: &#x27;mewfish&#x27;, &#x27;exp&#x27;: &#x27;2030-01-01T00:00:00+00:00&#x27;}</span>
</code></pre>
<p>В <strong>symmetic (local)</strong> режиме <code>jam.paseto</code> выполняет следующее (см. исходный код):</p>
<ul>
<li>Формирует ASCII-заголовок вида <code>v4.local.</code> (серийная строка версии и «назначения»).</li>
<li>Генерирует 24-байтовый случайный <code>nonce</code> (четвёртая часть шифрования XChaCha20-Poly1305).</li>
<li>Собирает AAD (additional authenticated data) как PAE([header, footer]) – специальная функция, объединяющая header и footer.</li>
<li>Вызывает <code>xchacha20poly1305_encrypt(secret, nonce, payload, aad)</code>, получая шифротекст. Затем токен = <code>header + base64url(nonce+ciphertext)</code>. Если есть footer, он также кодируется base64 и добавляется через точку.</li>
<li>При верификации происходит обратное: разбираются части, проверяется заголовок, расшифровывается с тем же <code>secret</code> и AAD. В случае повреждения или подделки недекодированный ciphertext приведёт к ошибке.</li>
</ul>
<p>В <strong>asymmetric (public)</strong> режиме применяется Ed25519-подпись:</p>
<ul>
<li>Заголовок <code>v4.public.</code> конкатенируется с сериализованным JSON-пayload.</li>
<li>Формируется <code>pre_auth = PAE([header, payload, footer])</code>.</li>
<li>Приватный ключ Ed25519 подписывает <code>pre_auth</code>, получая подпись (64 байта).</li>
<li>Токен = <code>header + base64url(payload+signature)</code> [+ опциональный <code>.footer</code>].</li>
<li>При декодировании библиотека проверяет подпись публичным ключом Ed25519: <code>public_key.verify(signature, pre_auth)</code>. Если проверка не проходит, бросается исключение о недопустимом токене.</li>
</ul>
<p>Таким образом, <strong>PASETO устраняет два класса ошибок JWT</strong>: во‑первых, здесь нет флажка типа алгоритма в токене (всё определяется версией), а, во‑вторых, используются проверенные схемы AEAD (шифрование+аутентификация) и EdDSA, минимизирующие опасность криптографических заплат. Особенно важно, что злоумышленник не может просто указать свой алгоритм: либо у него есть секретный ключ (local) - тогда он может <strong>расшифровать и подпись</strong>, но не подделать защищённые биты, либо у него есть только публичный ключ (public) - он никак не сможет создать корректную подпись.</p>
<p>Код <code>jam.paseto</code> ясно иллюстрирует эти механизмы. Например, при шифровании используется <code>xchacha20poly1305_encrypt</code> со 192-битным <code>nonce</code>, а при верификации Ed25519 фиксирует каждую часть: <code>PASETOv4.decode()</code> проверит префикс <code>v4.public.</code>, длину подписи 64 байта и вызовет <code>verify</code> на публичном ключе. Эти жёсткие проверки делают PASETO устойчивым к классическим JWT-атакующим паттернам.</p>
<h2>Чем PASETO лучше JWT</h2>
<p><strong>Безопасность.</strong> В PASETO встроено использование только современных шифров и подписей: TLS-сервером заявлено, что <em>PASETO мало вероятно использовать в небезопасном вид</em>. Отсутствует <code>alg:none</code> и двусмысленность алгоритмов. В PASETOv4 для шифрования по умолчанию применяется XChaCha20-Poly1305 (AEAD-режим), а для подписи – Ed25519 (EdDSA), которые считаются стойкими. JWT, напротив, всё ещё допускает множество вариантов алгоритмов (HS*, RS*, PS*, ES*) и требует от разработчика самому грамотно их выбирать - что часто идёт не так.</p>
<p><strong>Простота и надёжность.</strong> PASETO логически проще: только два режима (шифрование или подпись) и версии. JWT имеет JWS/JWE/JWK и тд - это делает его мощным, но и опасным. PASETO даже рекомендует: <em>Лучше использовать PASETO правильно, чем дать свободу на выбор алгоритмов</em>. Например, в PASETO нет полей <code>jku</code> или <code>kid</code>, поэтому атаки типа подмены ключей исключены априори.</p>
<p><strong>Агностицизм к алгоритму.</strong> В JWT разработчик указывает <code>&quot;alg&quot;</code>, и если у него избыточные права, он может допустить слабый вариант (например, ввести HMAC вместо RSA). В PASETO версия фиксирует алгоритм, и его нельзя поменять внутри токена. Этот принцип версионности (versioned protocols) описывается как контраст гибкости алгоритмов JWT - версионированные протоколы PASETO.</p>
<p><strong>Размер токена.</strong> PASETO-токены обычно получаются немного короче JWT-аналогов той же нагрузки, потому что структура проще и нет JSON-сериализованного заголовка (вместо него короткий ascii-префикс <code>v2.local.</code> и т.д.). Меньший размер полезен при передаче в заголовках или куках.</p>
<p><strong>Поддержка футеров.</strong> В PASETO можно использовать <code>footer</code> - доп. данные, не защищённые крипто-операциями (например, ID ключа или версия приложения). В <code>jam.paseto</code> footer передаётся явно при <code>encode</code> и обрабатывается (он подписывается вместе с payload). JWT имеет поле <code>kid</code> и другие расширения, но они менее стандартизированы и сложнее управляются.</p>
<p><strong>Экосистема.</strong> JWT поддерживается практически всеми фреймворками и библиотеками (OpenID Connect, OAuth2, готовые клиенты и т.п.). PASETO еще молодой стандарт, но постепенно набирает популярность - есть реализации на многих языках. Для Python, конечно же <a href="https://jam.makridenko.ru">Jam</a>, плюс есть <a href="https://pypi.org/project/paseto/">paseto</a> и <a href="https://pypi.org/project/pyseto/">pyseto</a>, но с ними надо быть аккуратно, они не строго следуют спецификации!</p>
<p>Ниже приведена сравнительная таблица основных характеристик JWT и PASETO:</p>
<table><thead><tr><th>Характеристика</th><th>JWT</th><th>PASETO</th></tr></thead><tbody><tr><td><strong>Архитектура</strong></td><td>JWS/JWE с полем <code>alg</code>, <code>typ</code> и другими.</td><td>Версионированные протоколы. Поле <code>alg</code> отсутствует, есть <code>version.purpose</code>.</td></tr><tr><td><strong>Алгоритм</strong></td><td>Пользователь выбирает из множества (HS256/RS256/PS256/ES256/none и др.), что может привести к ошибкам.</td><td>Фиксированные алгоритмы на каждую версию: в v4 – XChaCha20-Poly1305 (local) и Ed25519 (public) - неизменяемы и безопасны.</td></tr><tr><td><strong>Гибкость vs безопасность</strong></td><td>Максимальная гибкость, но большое поле для ошибок.</td><td>Жёсткие, безопасные умолчания, гибкость «версия⇄алгоритм» минимальна.</td></tr><tr><td><strong>Сложность</strong></td><td>Высокая: JWS + JWE + JWK, много необязательных полей, тонкостей.</td><td>Простая: только header+payload+(optional footer), используются AEAD и EdDSA.</td></tr><tr><td><strong>Атаки</strong></td><td>Уязвимы к <code>alg:none</code>, key confusion, JWKS/JKU-атакам и др..</td><td>Почти неуязвимы к этим: <code>none</code> нет, <code>alg</code> нет, ключи жёстко привязаны к версиям.</td></tr><tr><td><strong>Управление ключами</strong></td><td>Через JSON Web Keys, <code>kid</code>, <code>jku</code> - сложно и опасно при подменах.</td><td>Простой режим: симметрич. ключ для local, публичный ключ для public. Нет <code>kid</code>/<code>jku</code>.</td></tr><tr><td><strong>Размер токена</strong></td><td>Обычно больше (header JSON + payload JSON + подпись).</td><td>Меньше (короткий ascii-заголовок + base64(байты)).</td></tr><tr><td><strong>Среда</strong></td><td>Поддержка во всех веб-фреймворках, библиотечных экосистемах.</td><td>Поддержка растёт: есть основные реализации. Инструментарий менее распространён.</td></tr><tr><td><strong>Использование</strong></td><td>Часто неправильно используется как «сессионный» токен.</td><td>Явно ориентирован на безопасность. Не предназначен для автоматической снятой сессий (stateless).</td></tr></tbody></table>
<p>Как видно из таблицы, PASETO жертвует некоторой гибкостью JWT ради безопасности и простоты. При этом он устраняет большинство описанных проблем JWT по умолчанию, но от сюда следует вот такая картина:</p>
<p><img src="https://makridenko.com/imgs/auth-tokens-moscow-python/attack-all.svg" alt="attack"/></p>
<p>Мы видим, что в JWT из коробки больше паттернов для атаки, в PASETO мы всегда упираемся в подпись.</p>
<hr/>
<p>JWT – зрелый стандарт для веб-токенов, но с большой долей ответственности разработчика: он сам выбирает алгоритмы и опции, поэтому под неправильной рукой JWT легко превратить в большую дырку. Оценка рисков показывает: <strong>JWT легко использовать неверно</strong> – от <code>alg:none</code> до &quot;просто decode&quot; вместо verify - что может полностью скомпрометировать безопасность системы. К тому же JWT зачастую применяют там, где достаточно простого механизма аутентификации, усложняя архитектуру без выгоды.</p>
<p>PASETO предлагает более безопасный подход: чёткие версии, только современные алгоритмы, минимум конфигурации. Использование PASETO позволяет избежать типичных проблем: невозможно написать <code>alg:none</code>, библиотека сама корректно шифрует/подписывает и проверяет токен.</p>
<p>В итоге рекомендация для разработчиков: критически оценивать необходимость JWT. Если нужен простейший маркер авторизации, может быть достаточно короткого HMAC-токена или сессии с хранением на сервере. Если же требуется самодостаточный токен, стоит отдавать предпочтение PASETO или строго контролировать использование JWT (фиксировать алгоритмы, периодически ротировать ключи, проверять все поля).</p>
<p><strong>Материал:</strong></p>
<ul>
<li>спецификации <a href="https://datatracker.ietf.org/doc/html/rfc7519">JWT</a>/<a href="https://datatracker.ietf.org/doc/html/rfc7165">JOSE</a></li>
<li>анализ уязвимостей JWT (<a href="https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JSON_Web_Tokens">OWASP</a>, <a href="https://portswigger.net/web-security/jwt/algorithm-confusion">PortSwigger</a>) и <a href="https://github.com/paseto-standard/paseto-spec">PASETO</a></li>
<li>Документация Jam: <a href="https://jam.makridenko.ru">jam.makridenko.ru</a></li>
</ul>
<p>Ну и: <a href="https://makridenko.ru/presentations/mp-auth-tokens.html">презентация</a></p>]]></content:encoded>
      <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <enclosure url="https://makridenko.com/imgs/auth-tokens-moscow-python/attack-all.svg" type="image/svg+xml" length="0"/>
      <category>Paper</category>
      <category>Python</category>
    </item>
    <item>
      <title><![CDATA[Сборка мусора, ссылки и управление памятью.]]></title>
      <link>https://makridenko.com/posts/2026/04/16/gc</link>
      <guid>https://makridenko.com/posts/2026/04/16/gc</guid>
      <description><![CDATA[Это текстовая версия моего доклада для студентов ММА. Источники и презентация в конце. Языки с управляемой памятью (такие как Python) используют сборщик мусора (GC) для автоматического освобождения неиспользуемых объектов и предотвращения утечек памяти. Задачи GC – находить и освобождать память, не достижимую из «корней» (roots) программы, тем самым перераспределяя её для новых объектов. Несмотря на это, сборщики накладывают ограничения...]]></description>
      <content:encoded><![CDATA[<blockquote>
<p>Это текстовая версия моего доклада для студентов ММА.</p>
</blockquote>
<p>Языки с управляемой памятью (такие как Python) используют <strong>сборщик мусора</strong> (GC) для автоматического освобождения неиспользуемых объектов и предотвращения утечек памяти. Задачи GC – находить и освобождать память, не достижимую из «корней» (roots) программы, тем самым перераспределяя её для новых объектов. Несмотря на это, сборщики накладывают ограничения: они увеличивают накладные расходы (время и память), могут вызывать паузы исполнения и не гарантируют немедленного освобождения памяти (например, в случае циклических ссылок). Общая цель доклада – дать представление о необходимости GC: он делает программы надёжнее (нет забытых <code>free</code>), но требует понимания компромиссов между пропускной способностью и задержкой.</p>
<p><strong>Ключевые задачи GC:</strong> вернуть память неиспользуемых объектов системе, минимизировать фрагментацию и издержки в производительности. <strong>Ограничения:</strong> паузы (stop-the-world), сложности с циклами, накладные расходы на хранение метаданных. В результате, GC – это баланс между надёжным управлением памятью и контролем производительности приложения.</p>
<h1>Основные концепции GC на уровне памяти</h1>
<p><strong>Управление памятью:</strong> В процессе работы программа использует несколько сегментов памяти: <em>статический</em> (код и глобальные данные), <em>кучу</em> (динамические объекты) и <em>стек</em> (фреймы функций, локальные переменные). Сборщик мусора <strong>управляет только кучей</strong> – памятью, выделенной динамически. Память стека освобождается автоматически при выходе из функции; статические объекты и глобальные переменные живут в течении всего выполнения.</p>
<p><strong>Корни (roots) и достижимость:</strong> «Корнем» называют объект, до которого виртуальная машина может добраться напрямую (без прохода через другие объекты). В качестве корней выступают, например, локальные переменные на стеке, глобальные объекты, содержимое регистров и стека выполнения. Объект называется <strong>достижимым</strong>, если от корня к нему ведёт цепочка ссылок. Невозможно достижимые объекты расцениваются как «мусор» и подлежат сбору. Таким образом, сборщик начинает с корней и помечает все объекты, до которых можно добраться, а затем удаляет все непромеченные.</p>
<p><strong>Объекты и указатели:</strong> В управляемых языках объекты (все экземпляры) хранятся в куче, а переменные и поля объектов представляют собой указатели (ссылки) на эти объекты. Например, если объект <code>A</code> содержит ссылку на объект <code>B</code>, то в «графе объектов» есть ребро <code>A → B</code>. При выполнении сборки мусора строится ориентированный граф из всех объектов и отслеживаются ссылки.</p>
<p><strong>Области видимости и граф объектов:</strong> Переменные, доступные из активного кода (локальные, глобальные, статику), являются корнями, а вся совокупность объектов и их ссылок образует граф.</p>
<p><img src="https://makridenko.com/imgs/garbage.png" alt="garbage"/></p>
<p>В этом примере из корня доступны <code>Obj1</code>, <code>Obj2</code>, <code>Obj3</code> (они достижимы). Любой объект вне такой цепочки (непомеченный) считается мусором. <em>Достижимость</em> определяется индукцией: корни достижимы по определению, а любой объект, на который указывает достижимый, тоже достижим.</p>
<p>Таким образом, сборщик мусора реализует обход объекта (обычно метка‑освобождение) от корней и освобождает всё, что не достигнуто. Этот основной принцип лежит в основе большинства алгоритмов GC.</p>
<h1>Типы сборщиков</h1>
<p>Существует несколько подходов к сборке мусора. Ниже приведена таблица ключевых типов GC с их принципом работы, плюсами, минусами и типичными сценариями применения:</p>
<table><thead><tr><th>Тип сборщика</th><th>Принцип работы</th><th>Преимущества</th><th>Недостатки</th></tr></thead><tbody><tr><td><strong>Подсчёт ссылок</strong></td><td>Для каждого объекта ведётся счётчик ссылок. При создании/удалении ссылки счётчик инкрементируется/декрементируется. Как только счётчик достигает 0, объект сразу освобождается.</td><td>Объект сразу освобождается при последней ссылке Реальное время (каждая операция ограничена) Простота реализации</td><td>Не справляется с циклическими ссылками (счётчик никогда не станет нулём) Накладные расходы на поддержание счётчиков (память и время) Медленно для краткоживущих объектов (много операций)</td></tr><tr><td><strong>Компактирующий (Mark–Compact)</strong></td><td>После метки всех живых объектов все они <strong>перемещаются</strong> в начало кучи плотно, а остаток памяти освобождается в одном большом блоке.</td><td>– Устраняет фрагментацию (вся свободная память–сплошной блок) Поддерживает упорядоченность объектов</td><td>– Ещё более затратный по времени, чем простая метка‑сбор (надо перемещать объекты) Требует обновления всех указателей на перемещённые объекты (сложно)</td></tr><tr><td><strong>Копирующий</strong></td><td>Куча делится пополам на полупространства (from-space и to-space). Сборка: копируются <strong>только помеченные (живые)</strong> объекты из from-space в to-space, остальные остаются в мусоре. Затем меняют роли: to-space становится новой кучей.</td><td>Устраняет фрагментацию (все объекты уплотнены в to-space) Время сборки пропорционально числу живых объектов (часто быстро, если живых мало)</td><td>Нужен дополнительный объём памяти ~с размер кучи (нужно полупространство) Требует обновления всех перемещённых указателей</td></tr><tr><td><strong>Поколенческий (Generational)</strong></td><td>Гипотеза поколений: большинство объектов умирают молодыми. Кучу разбивают на «молодое» поколение и «старое». Собирают молодое поколение очень часто, старое – редко. Выжившие объекты постепенно «продвигают» в старшее поколение.</td><td>Значительно уменьшает объём работы при сборке (каждый раз анализируется только часть кучи) Поддерживает короткие паузы (обычно только сбор «молодого» поколения)</td><td>Усложняет реализацию: нужно отслеживать ссылки из старого поколения в молодое (write-barrier) В худшем случае (много «старых» ссылок) эффективность снижается</td></tr><tr><td><strong>Консервативный</strong></td><td>Сборщик не знает точного расположения указателей в памяти. При обходе стека и куче он считает <strong>любое значение, похожее на адрес кучи</strong>, как указатель (с некоторыми допущениями).</td><td>Не требует изменения существующего кода (не нужно хранить точные карты корней) Может работать с языками без богатой информации о типах (например, С/C++)</td><td>Может ошибочно принять целое число за указатель и не освободить реальный мусор(ложноположительные при сканировании) Обычно не позволяет компактировать память (из-за неопределённости)</td></tr><tr><td><strong>Точный (Precise)</strong></td><td>GC знает точное расположение всех указателей (есть метаданные или компилятор). При сборке сканируются именно указатели, и коллекция «безопасна» и эффективна.</td><td>Полная уверенность в корректном освобождении всех «мертвых» объектов Позволяет перемещать объекты и компактировать память. Возможна реализация поколенческого GC</td><td>Требует сложных инструментов (генерация описателей стека или <code>shadow stack</code>) Дополнительные издержки (например, хранение карт или структуры данных для корней)</td></tr></tbody></table>
<p><strong>Краткий вывод:</strong> <em>Reference counting</em> прост и оперативен, но не справляется с циклами. <em>Метод «метка–сбор»</em> надёжный и не требует доп. памяти, но может вызывать фрагментацию и паузы. <em>Копирующие и поколенческие</em> GC уменьшают паузы и фрагментацию за счёт перераспределения памяти и частичных сборок. <em>Консервативные сборщики</em> применимы в системах без информации о корнях (C/C++). В Python главным образом используется сочетание подсчёта ссылок (точный GC) и поколенческого алгоритма для циклов.</p>
<h1>Алгоритмы и схемы</h1>
<p>Ниже приведены алгоритмы основных схем GC с шагами их работы, схемами и оценками сложности.</p>
<h2>Алгоритм «метка–сбор» (Mark–Sweep)</h2>
<ol>
<li><strong>Метка (Mark):</strong> начинаем с корней, рекурсивно помечаем все достижимые объекты.</li>
<li><strong>Сбор (Sweep):</strong> проходим по всей куче и освобождаем (удаляем) все объекты, не помеченные как достижимые.</li>
</ol>
<p><img src="https://makridenko.com/imgs/gc-mark.png" alt="gc-mark"/></p>
<p><strong>Пример:</strong> Пусть корень ссылается на <code>A</code>, которое ссылается на <code>B</code>, и <code>B</code> на <code>C</code>, а объекты <code>D</code> и <code>E</code> изолированы. После метки <code>A, B, C</code> пометятся, а <code>D</code> и <code>E</code> будут собраны.</p>
<p><strong>Сложность:</strong> Время – <em>O(N)</em> (сканирование всей кучи дважды: пометка и сбор). Память – минимальные накладные (обычно всего несколько бит для маркировки).</p>
<h2>Алгоритм компакции (Mark–Compact)</h2>
<p>Это вариация Mark–Sweep. После пометки живых объектов вместо удаления формируются компактные «уплотнённые» области:</p>
<ol>
<li><strong>Метка:</strong> как выше.</li>
<li><strong>Компактирование:</strong> все помеченные объекты копируются в начало кучи (сохраняя относительный порядок), непромеченные объекты «сдвигаются» и вся свободная область становится непрерывной.</li>
</ol>
<p><img src="https://makridenko.com/imgs/gc-mark-compact.png" alt="mark-compact"/></p>
<p><strong>Сложность:</strong> Время – обычно больше, чем у простого Mark–Sweep (требуется перемещение, поиск новых адресов для указателей). Область памяти – дополнительная метка, но сама алгоритм не требует двойного пространства (в отличие от копирования).</p>
<h2>Копирующий сборщик (Copying Collector)</h2>
<p>Куча делится пополам на два полупространства. Алгоритм:</p>
<ol>
<li><strong>Пометка и копирование:</strong> из исходного полупространства (from-space) все достижимые объекты копируются в свободное полупространство (to-space). При этом удаляются объекты, не скопированные.</li>
<li><strong>Перемещение:</strong> после копирования полупространства меняются ролями (now to-space становится активной кучей).</li>
</ol>
<p><img src="https://makridenko.com/imgs/gc-mark-copy.png" alt="mark-copy"/></p>
<p><strong>Сложность:</strong> Время пропорционально количеству живых объектов (если их мало, быстро). Недостаток – требуется дополнительное пространство (~двойной объём кучи) для второго полупространства. После копирования куча не фрагментируется.</p>
<h2>Поколенческий сборщик (Generational GC)</h2>
<p>Поколенческий GC объединяет «метка–сбор» с разделением кучи на поколения по возрасту объектов. Обычно реализуется так:</p>
<ul>
<li><strong>Инициализация:</strong> объекты сначала попадают в <em>молодое</em> поколение. Когда выполняется сборка, проверяются только объекты в молодом поколении (minor GC).</li>
<li><strong>Продвижение:</strong> объекты, пережившие несколько сборок, переносятся в <em>старшее</em> поколение.</li>
<li><strong>Сбор старших:</strong> старшее поколение собирается реже (major GC) и часто вместе со всеми младшими.</li>
</ul>
<p><img src="https://makridenko.com/imgs/gc-mark-gen.png" alt="mark-generation"/></p>
<p><strong>Сложность:</strong> Ускоряет GC: большинству объектов не нужна частая проверка. Сбор малого поколения быстрый (обычно O(M) для небольшого M). Недостаток – сложная реализация (нужно отслеживать ссылки из старого в молодой, «барьеры записи»). CPython, начиная с версии 3.14, использует два поколения (молодое и старое). Молодое поколение собирается чаще, старшее – постепенно (обычно 1% при пороге по умолчанию).</p>
<h1>Структура объекта в памяти</h1>
<p>В управляемых языках объекты имеют <strong>заголовок</strong> с метаданными. В CPython (управляемой памятью) каждый объект на низком уровне представлен структурой C:</p>
<ul>
<li><strong>ob_refcnt:</strong> счётчик ссылок на объект (тип <code>Py_ssize_t</code>), который увеличивается/уменьшается при присваивании/удалении ссылок.</li>
<li><strong>ob_type:</strong> указатель на дескриптор типа (структуру <code>PyTypeObject</code>), содержащую информацию о классе объекта.</li>
<li><strong>(для переменной длины) ob_size:</strong> если объект имеет переменный размер (например, списки), здесь хранится длина.</li>
</ul>
<p>В <strong>официальной документации</strong> отмечено: «в обычной сборке релиза [PyObject] содержит только счётчик ссылок и указатель на соответствующий объект типа». Пример псевдокода C-структуры:</p>
<pre><code class="hljs language-c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> {</span>
    Py_ssize_t ob_refcnt;       <span class="hljs-comment">// Счётчик ссылок</span>
    PyTypeObject *ob_type;      <span class="hljs-comment">// Указатель на тип объекта</span>
    <span class="hljs-comment">// ... далее поля объекта ...</span>
} PyObject;

<span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> {</span>
    PyObject ob_base;           <span class="hljs-comment">// базовая часть PyObject</span>
    Py_ssize_t ob_size;         <span class="hljs-comment">// размер для объектов переменной длины</span>
    <span class="hljs-comment">// ... далее поля списка или другого контейнера ...</span>
} PyVarObject;
</code></pre>
<p>В <strong>урезанной реализации CPython</strong> (если объект учтён GC) перед этими полями выделяется скрытый заголовок <code>PyGC_Head</code>, который содержит указатели <code>_gc_next</code> и <code>_gc_prev</code> для двусвязного списка контейнеров GC. Этот заголовок позволяет <strong>генерационному GC</strong> отслеживать все объекты типа «контейнер» (например, списки, словари) в своих списках по поколениям.</p>
<p>Выравнивание адресов в CPython обычно соответствует размеру машинного слова (8 байт на 64-бит), поэтому сами поля выровнены. В итоге, указатель на объект (<code>PyObject *</code>) фактически указывает на начало этой структуры (включая <code>ob_refcnt</code>). Метаданные (тип объекта) в статической памяти содержат таблицы методов и размеры полей.</p>
<p>Таким образом, сборщик мусора знает, где находится счётчик <code>ob_refcnt</code> (он в начале объекта) и где тип, что позволяет ему корректно управлять ссылками и освобождать объект, когда счётчик падает до 0. Обратите внимание: поскольку объекты не перемещаются (их адрес не меняется после аллокации), ссылкой на объект выступает просто указатель <code>PyObject*</code>.</p>
<h1>GC в CPython</h1>
<p>CPython использует <strong>гибридную схему</strong>: базовый механизм – <strong>подсчёт ссылок</strong>, дополняющийся <strong>поколенческой сборкой циклов</strong>.</p>
<ul>
<li>
<p><strong>Подсчёт ссылок:</strong> Каждый объект хранит поле <code>ob_refcnt</code>. Операции <code>Py_INCREF</code>/<code>Py_DECREF</code> автоматически выполняются при манипуляции ссылками. Когда <code>ob_refcnt</code> объекта достигает 0, CPython немедленно освобождает память этого объекта (вызывает деструктор и возвращает блок памяти). Это происходит во время исполнения программы, без дополнительных пауз.</p>
</li>
<li>
<p><strong>Генерационная сборка:</strong> Для обнаружения циклов CPython использует опциональный GC на основе поколения. Существуют связанные списки всех контейнерных объектов по поколениям (в CPython 3.14 их два – молодое и старое). Все новые объекты помещаются в <em>молодое</em> поколение. Периодически (по счётчику операций аллокации) выполняется сборка поколения 0 (малого); при этом объекты, пережившие несколько сборок, продвигаются в старшее поколение. При вызове <code>gc.collect()</code> без аргументов или с <code>generation=2</code> выполняется полный сбор (молодое + старое), а с <code>generation=0</code> – только молодого. Для сборки циклов CPython использует метод <em>три-цветной маркировки</em>, реализованный с сохранением копии счётчиков: сначала копируется <code>ob_refcnt</code> каждого проверяемого объекта в специальное поле GC-заголовка, затем уменьшаются внутренние ссылки внутри поколения (фазы <em>subtract_refs</em>), и те объекты, у которых скопированный счётчик стал ≤0, считаются недостижимыми и удаляются.</p>
</li>
</ul>
<p>Диаграмма простого цикла в CPython:</p>
<p><img src="https://makridenko.com/imgs/gc-cycl-ref.png" alt="cycl-ref"/>
Циклическая ссылка: оба объекта взаимно достижимы между собой, но после удаления всех внешних ссылок их счётчики не станут 0. Генерационный GC обнаружит такую группу.</p>
<p>В <strong>CPython 3.12</strong> сборка циклов происходила при выполнении определённого числа операций аллокации. Начиная с <strong>CPython 3.12</strong>, механизм запуска GC изменён: теперь GC срабатывает не при каждой аллокации, а на каждые <em>брейкер-флаги</em> байткода (каждый шаг цикла eval-loop), а также при проверке сигналов <code>PyErr_CheckSignals</code> в длинноработающих C-расширениях. Это даёт более равномерные точки запуска GC и уменьшает влияние сборщика на производительность.</p>
<h2>Примеры в CPython</h2>
<p>Ниже показаны примеры на Python 3.12+ с комментариями, демонстрирующие подсчёт ссылок и GC циклов.</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">class</span> <span class="hljs-title class_">A</span>:
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, name</span>):
        <span class="hljs-variable language_">self</span>.name = name
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__del__</span>(<span class="hljs-params">self</span>):
        <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;Deleted <span class="hljs-subst">{self.name}</span>&quot;</span>)

<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;=== Reference Counting ===&quot;</span>)
a = A(<span class="hljs-string">&quot;a1&quot;</span>)
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Объект a создан&quot;</span>)
<span class="hljs-keyword">del</span> a
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Удалили a, __del__ должен был сработать сразу&quot;</span>)
</code></pre>
<p><strong>Ожидаемый вывод:</strong></p>
<pre><code>=== Reference Counting ===
Объект a создан
Deleted a1
Удалили a, __del__ должен был сработать сразу
</code></pre>
<p>Объект <code>a1</code> создаётся и у него <code>ob_refcnt=1</code>. После <code>del a</code> счётчик становится 0, и CPython сразу вызывает <code>a.__del__()</code>, выводя <code>Deleted a1</code>. Это демонстрирует детерминированное освобождение при подсчёте ссылок.</p>
<pre><code class="hljs language-python"><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;\n=== Циклическая ссылка без финализатора ===&quot;</span>)
a = A(<span class="hljs-string">&quot;A&quot;</span>)
b = A(<span class="hljs-string">&quot;B&quot;</span>)
a.other = b
b.other = a
<span class="hljs-keyword">del</span> a, b
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Ссылки a и b удалены, вызываем сборку&quot;</span>)
<span class="hljs-keyword">import</span> gc
gc.collect()
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;GC завершён&quot;</span>)
</code></pre>
<p><strong>Ожидаемый вывод (Python 3.4+):</strong></p>
<pre><code>=== Циклическая ссылка без финализатора ===
Ссылки a и b удалены, вызываем сборку
Deleted A
Deleted B
GC завершён
</code></pre>
<p>Здесь мы создаём цикл <code>A↔B</code>. После <code>del a, b</code> внешних ссылок нет, но без GC у классов с <code>__del__</code> цикл раньше не собирался. Начиная с Python 3.4 (PEP 442), CPython всё же собирает такие объекты и вызывает их <code>__del__</code>. Поэтому после <code>gc.collect()</code> оба объекта освобождаются и печатают свои сообщения. Если бы <code>__del__</code> отсутствовал, объекты просто удалились бы без вывода.</p>
<pre><code class="hljs language-python"><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;\n=== Цикл со слабой ссылкой ===&quot;</span>)
<span class="hljs-keyword">import</span> weakref
<span class="hljs-keyword">class</span> <span class="hljs-title class_">B</span>:
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, name</span>):
        <span class="hljs-variable language_">self</span>.name = name
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__repr__</span>(<span class="hljs-params">self</span>):
        <span class="hljs-keyword">return</span> <span class="hljs-string">f&quot;&lt;B <span class="hljs-subst">{self.name}</span>&gt;&quot;</span>

e = B(<span class="hljs-string">&quot;e&quot;</span>)
f = B(<span class="hljs-string">&quot;f&quot;</span>)
e.f = f
f.e = e
wr = weakref.ref(e, <span class="hljs-keyword">lambda</span> x: <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;<span class="hljs-subst">{x}</span> collected&quot;</span>))
<span class="hljs-keyword">del</span> e, f
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;После del e,f создаём цикл, weakref ждёт сборки&quot;</span>)
gc.collect()
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;GC завершён, weakref вернул:&quot;</span>, wr())
</code></pre>
<p><strong>Ожидаемый вывод:</strong></p>
<pre><code>=== Цикл со слабой ссылкой ===
После del e,f создаём цикл, weakref ждёт сборки
&lt;B e&gt; collected
GC завершён, weakref вернул: None
</code></pre>
<p>Здесь создаётся цикл между <code>e</code> и <code>f</code>. Мы сохраняем слабую ссылку <code>wr</code> на объект <code>e</code> с колбэком, чтобы увидеть момент сбора. После <code>del</code> и <code>gc.collect()</code> цикл будет удалён, weakref вызовет колбэк (<code>&lt;B e&gt; collected</code>) и вернётся <code>None</code>. Это показывает, как <code>weakref</code> помогает обнаруживать освобождение объектов, избегая собственно цикла в Python-объектах.</p>
<p>Таким образом, на практике GC в CPython комбинирует немедленное удаление при <code>refcount=0</code> и фоновый сбор циклов. При работе с ресурсами важно помнить о возможных циклах: либо избегать их, либо использовать <code>weakref</code>/<code>gc</code> модуль для контроля.</p>
<h1>История и изменения в CPython 3.12→3.13→3.14</h1>
<ul>
<li>
<p><strong>Python 3.12:</strong> Сборщик циклов <strong>переведён на новый механизм запуска</strong>. Вместо проверки после каждой аллокации, GC теперь срабатывает на каждом байткод-брейкере (каждый шаг eval loop) и при проверке сигналов. Это сглаживает накладные расходы и устраняет «горячие точки» сборки, заметно снижая задержки в некоторых сценариях.</p>
</li>
<li>
<p><strong>Python 3.13:</strong> На предварительных релизах (3.13.0rc1) была <strong>введена экспериментальная инкрементальная сборка GC</strong> – сборщик разделял работу на небольшие части, чтобы ещё больше сократить паузы. Однако перед финальным релизом 3.13 эта инкрементальность была <strong>откочена</strong> из-за регрессий производительности (замедление Sphinx). Иными словами, в 3.13 изменений в механизме генерационной сборки не произошло, а новая функция была удалена («отложена» в 3.14). Также в 3.13 включён альтернативный аллокатор <code>mimalloc</code>, но это касается памяти, а не логики GC.</p>
</li>
<li>
<p><strong>Python 3.14:</strong> <strong>Поколенческий сборщик</strong> наконец стал <strong>инкрементальным по умолчанию</strong>. Это означает, что теперь сборка циклов может разбиваться на мелкие шаги во время исполнения, что значительно сокращает максимальные паузы и делает работу сборщика более предсказуемой. CPython 3.14 также <strong>упростил поколенческую модель</strong>: осталось два поколения (молодое и старое) вместо трёх. При этом генерация 1 (среднего возраста) фактически была удалена – в <code>gc.get_objects()</code> для 3.14 видно, что <code>generation=1</code> не возвращает объектов. Кроме того, изменены пороги сборки: теперь <code>threshold2</code> игнорируется, а параметры по умолчанию подобраны для инкрементального сбора (в т. ч. среднее сканирование старого поколения ≈1% за проход).</p>
</li>
</ul>
<p><strong>До/после:</strong></p>
<ul>
<li><em>Было (до 3.12)</em> – обычная поколенческая сборка при каждой N-й аллокации.</li>
<li><em>Стало (3.12)</em> – сборка на каждом байткоде и при сигнал-чеках (устранено влияние аллокаций).</li>
<li><em>Добавлено (3.13 RC1)</em> – <strong>инкрементальность</strong> (удалено).</li>
<li><em>Возвращено (3.14)</em> – инкрементальный (по шагам) GC для циклов, сокращены поколения, обновлены пороги.</li>
</ul>
<p><strong>Рекомендации при миграции:</strong> при переходе с 3.11→3.12 и далее важно проверять время работы коллектора и влияние на паузы. Например, профилировать критичные участки до/после 3.12 для подтверждения отсутствия новых «горячих точек». При переходе 3.13→3.14 следует <strong>особенно протестировать задержки GC</strong>: 3.14 по умолчанию даёт более равномерные по времени сборки, но при определённых нагрузках требуется перенастройка порогов <code>gc.set_threshold()</code> для оптимального баланса. Возможен регрессионный код, если он ожидал специфичного поведения GC (например, ручной <code>gc.collect(1)</code> в 3.14 уже не делает полноценного прохода).</p>
<p>Ниже приведена схема двух поколений в 3.14 и изменённого поведения <code>gc.collect()</code>:</p>
<pre><code class="hljs language-text">   [Ввод нового объекта] --&gt; помещается в поколение 0 (молодое)

   Порог сборки поколения 0 превышен →
      Сборка: метка/сбор поколения 0 (и часть поколения 2) [по умолчанию 1%].

   Объекты, пережившие N сборок → продвигаются в поколение 2 (старое).
</code></pre>
<p>Если вручную вызвать <code>gc.collect(0)</code>, сборится только молодое поколение; <code>gc.collect(2)</code> или без аргумента – полная сборка. В 3.14 параметр <code>gc.collect(1)</code> больше не существует (и игнорируется).</p>
<h1>Практические советы и отладка</h1>
<p><strong>Инструменты Python:</strong></p>
<ul>
<li>Модуль <code>gc</code>: позволяет включать/отключать сборку (<code>gc.enable/disable()</code>), менять пороги (<code>gc.set_threshold()</code>), вызывать сборку вручную (<code>gc.collect()</code>), получать статистику (<code>gc.get_stats()</code> возвращает количество сборов/объектов) и просматривать «трупы» (<code>gc.garbage</code>, содержит объекты, недоступные для освобождения). Также <code>gc.get_referrers()</code> помогает понять цепочки ссылок.</li>
<li><code>tracemalloc</code>: модуль для профилирования распределения памяти (отслеживает, где и сколько памяти выделено). Полезен для поиска утечек и «горячих» мест по выделению памяти.</li>
<li>Внешние библиотеки: <code>objgraph</code> (визуализация графа объектов, поиск циклов), <code>Heapy</code>/<code>Guppy</code> (анализ размеров объектов), <code>memory_profiler</code>.</li>
<li>Отладка: флаги <code>gc.set_debug(gc.DEBUG_STATS|DEBUG_SAVEALL)</code> выводят подробности о сборках (когда и что собрано, непригодные объекты попадают в <code>gc.garbage</code>). Это помогает находить, какие типы объектов создают циклы.</li>
</ul>
<p><strong>Советы при написании кода:</strong></p>
<ul>
<li><strong>Избегайте ненужных циклов:</strong> по возможности «разрывайте» циклические ссылки, особенно если задействуются объекты с <code>__del__</code>. Используйте слабые ссылки (<code>weakref</code>) там, где объекты могут ссылаться друг на друга (например, кэш, графы).</li>
<li><strong>Контекстные менеджеры:</strong> для ресурсов (файлы, соединения) используйте <code>with</code>, чтобы гарантировать освобождение, а не полагайтесь на GC.</li>
<li><strong>Явное удаление:</strong> <code>del obj</code> можно использовать для явного уменьшения счётчика и ускорения GC (особенно в циклах), однако в большинстве случаев достаточно просто потерять ссылку.</li>
<li><strong>Оптимальное использование builtins:</strong> некоторые встроенные типы имеют собственные free-листы (например, маленькие кортежи, списки) – очищайте их явным <code>gc.collect()</code>, если нужно уменьшить пиковое потребление.</li>
<li><strong>Низкоуровневое управление:</strong> при экстремальной оптимизации можно переключать аллокаторы (<code>PYTHONMALLOC</code>), но это выходит за рамки GC.</li>
</ul>
<p><strong>Профилирование GC:</strong> измерять время сборки можно вставкой таймеров вокруг <code>gc.collect()</code> или использовать <code>gc.callbacks</code> (Python 3.3+) для отслеживания начала/конца каждой сборки. Внешние профайлеры (например, PyInstrument) также могут показать влияние GC на общее время.</p>
<hr/>
<p>GC облегчает работу программиста, освобождая от ручного управления памятью, но требует понимания своих ограничений. Важно знать базовые понятия (достижимость, циклы) и архитектуру GC своего языка. В Python основной механизм – подсчёт ссылок – делает освобождение объектов почти мгновенным, а дополнительный поколенческий GC решает проблемы циклов (с низкими накладными). Современные версии CPython (3.12–3.14) оптимизируют запуск GC для сокращения пауз. Для углубления рекомендую следующие ресурсы:</p>
<ul>
<li><a href="https://docs.python.org/3/c-api/memory.html#:~:text=Memory%20management%20in%20Python%20involves,sharing%2C%20segmentation%2C%20preallocation%20or%20caching"><em>Memory Management — Python/C API reference manual</em></a>, раздел <strong>Overview</strong> (официальная документация).</li>
<li><a href="https://docs.python.org/3/library/gc.html#:~:text=threshold0%20to%20zero%20disables%20collection"><em>Garbage Collector interface (<code>gc</code> module)</em></a> – официальная документация Python 3.12+.</li>
<li><a href="https://docs.python.org/3/whatsnew/3.14.html#:~:text=Incremental%20garbage%20collection%C2%B6"><em>What’s New in Python 3.12, 3.13, 3.14</em></a> – официальные релиз-ноты Python (главы про GC).</li>
<li>Jones R., Lins R. <strong>Garbage Collection: Algorithms for Automatic Dynamic Memory Management</strong> (Wiley, 1996) – классический учебник по алгоритмам сборки мусора.</li>
<li>Jones R., Hosking A., Moss E. <strong>The Garbage Collection Handbook: The Art of Automatic Memory Management</strong> (CRC Press, 2011).</li>
<li>Насутити К. <em>Отладка утечек памяти в Python</em> – обзоры и статьи (например, про <code>tracemalloc</code> и <code>objgraph</code>).</li>
</ul>
<p>Презентация <a href="https://makridenko.com/presentations/gc.html">тут</a>.</p>]]></content:encoded>
      <pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <enclosure url="https://makridenko.com/imgs/garbage.png" type="image/png" length="0"/>
      <category>Python</category>
      <category>Paper</category>
    </item>
    <item>
      <title><![CDATA[Moscow Python №110]]></title>
      <link>https://makridenko.com/posts/2026/04/14/moscowpython110</link>
      <guid>https://makridenko.com/posts/2026/04/14/moscowpython110</guid>
      <description><![CDATA[Приходите послушать меня (и других спикеров, конечно же) на Moscow Python 20 апреля. Я расскажу...]]></description>
      <content:encoded><![CDATA[<p>Приходите послушать меня (и других спикиров, конечно же) на <strong>Moscow Python 20 апреля</strong>. Я расскажу о теме, которой уже проел плешь всем своим знакомым: JWT и PASETO.</p>
<ul>
<li>Регистрация: <a href="https://moscowpython.ru/">moscowpython.ru</a></li>
<li>Прямой эфир и запись: <a href="https://www.youtube.com/moscowdjangoru">youtube.com</a></li>
</ul>]]></content:encoded>
      <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <category>Random</category>
      <category>Python</category>
    </item>
    <item>
      <title><![CDATA[Привет мир!]]></title>
      <link>https://makridenko.com/posts/2026/04/04/hello-world</link>
      <guid>https://makridenko.com/posts/2026/04/04/hello-world</guid>
      <description><![CDATA[Воскрешаю свой блог...]]></description>
      <content:encoded><![CDATA[<p>Решил воскресить свой блог, так как стал часто писать в разные места свои интересные и не очень мысли, решено делать это тут.
Тут же буду весть devlog по <a href="https://jam.makridenko.ru">Jam</a>.</p>]]></content:encoded>
      <pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <category>Random</category>
    </item>
    <item>
      <title><![CDATA[PEP-810 и тотальная тряска]]></title>
      <link>https://makridenko.com/posts/2026/01/03/pep810</link>
      <guid>https://makridenko.com/posts/2026/01/03/pep810</guid>
      <description><![CDATA[Относительно недавно приняли PEP-810 и началась ТОТАЛЬНАЯ ТРЯСКА, и я ее не понимаю. PEP-810 - это нововведение, которое добавляет в Python поддержку явных ленивых импортов...]]></description>
      <content:encoded><![CDATA[<blockquote>
<p>Это репост моей статьи с Хабра</p>
</blockquote>
<p>Относительно недавно приняли <a href="https://peps.python.org/pep-0810/">PEP-810</a> и началась <strong>ТОТАЛЬНАЯ ТРЯСКА</strong>, и я ее не понимаю.</p>
<p><strong>PEP-810</strong> - это нововведение, которое добавляет в Python поддержку явных ленивых импортов. Да-да, теперь можно писать:</p>
<pre><code class="hljs language-python">lazy <span class="hljs-keyword">import</span> jamlib
</code></pre>
<p>и модуль <code>jamlib</code> не будет загружен до тех пор, пока ты реально не воспользуешься им в коде. Это может казаться мелочью, но на самом деле - большое изменение для всей экосистемы Python.</p>
<h2>Зачем и кому это надо?</h2>
<p>Почти все, что написано на python страдает проблемой долгого старта, особенно CLI инструменты. Даже если делаешь <code>some-cli --help</code> это может затянуться, т.к. питон начнет подгружать <strong>ВСЕ</strong> что объявлено в импортах, даже если <code>--help</code> никак их не трогает. Ленивые импорты решают эту проблему:</p>
<ul>
<li>Не обращаешься к <code>somelib</code> - он не грузится</li>
<li>Не используешь какой-то модуль - он не грузится в <code>sys.modules</code></li>
<li>Не используешь функцию - она не грузится в память</li>
<li><code>gc</code> не тратит 100 лет на выгребание всего этого мусора после старта</li>
</ul>
<p>Получаем:</p>
<ul>
<li>быстрое время старта</li>
<li>меньше поедаем памяти</li>
<li>не исполняем код из импорта лишний раз</li>
</ul>
<h2>Как это работает?</h2>
<p>В python введена новая синтаксическая конструкция <code>lazy &lt;import_action&gt; &lt;module&gt;</code>, например <code>lazy import math</code> или <code>lazy from jamlib import Jam</code> (Так же введен аргумент запуска <code>python -X lazy_imports=all</code>)</p>
<p>При этом:</p>
<ul>
<li>Ленивые импорты работают только на глобальном уровне, в функции и классы их не прокинуть,</li>
<li>Импортированный объект сначала - это прокси, который при первом обращении делает реальный импорт и заменяет себя настоящим объектом.</li>
</ul>
<p>Пример:</p>
<pre><code class="hljs language-python">lazy <span class="hljs-keyword">import</span> jamlib
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;jamlib&quot;</span> <span class="hljs-keyword">in</span> sys.modules)  <span class="hljs-comment"># False</span>
<span class="hljs-built_in">print</span>(jamlib.__version__)  <span class="hljs-comment"># Модуль явно вызывается</span>
<span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;jamlib&quot;</span> <span class="hljs-keyword">in</span> sys.modules)  <span class="hljs-comment"># True</span>
</code></pre>
<h2>И выглядит круто и на деле круто, но…</h2>
<p>…в коммьюнити(<em>особенно в русскоязычном</em>) началась ТРЯСКА, я не буду тыкать пальцем в конкретных людей/каналы/посты, но я думаю вы видели этот поток возмущения, вот основные претензии:</p>
<p><img src="https://makridenko.com/imgs/meme.jpg" alt="meme"/></p>
<blockquote>
<p>Зачем нам ещё одно ключевое слово?!</p>
</blockquote>
<p>Многие в сообществе возмутились, что <code>lazy</code> засоряет синтаксис. Мол, Python и так превращается в свалку ключевых слов (<code>async</code>, <code>match</code>, теперь ещё <code>lazy</code>...). Было предложение использовать <code>defer import</code>, <code>import(lazy)</code>, или вообще ничего не менять и продолжать писать <code>import</code> внутри функции.</p>
<p>Но это смешно, <strong>не нужно - не используй</strong>. Вот и все.</p>
<blockquote>
<p>Ошибки теперь будут позже</p>
</blockquote>
<p>Если без <code>lazy</code> ошибка импорта выскочит сразу, при запуске, то с <code>lazy</code> только когда модуль будет явно использован.</p>
<p>О ужас, это же реально все меняет (нет)! Для такого аргумента у меня один и тот же ответ: пиши тесты и код проверяй перед тем как лить куда-то.</p>
<blockquote>
<p>Импорты имеют побочные эффекты</p>
</blockquote>
<p>Да, импорты могут делать что-то важное при инициализации, но просто не засовывай их в <code>lazy</code> и не будет проблем.</p>
<blockquote>
<p>Сложность растет</p>
</blockquote>
<p>УУУУ СТАЛО СЛОЖНО, это же целое ключевое слово, лишняя магия. Давайте тогда вообще язык не развивать? Про типы вне <code>typing</code> тоже вой стоял, а сейчас хлебом не корми, дай тайпинга насыпать.</p>
<hr/>
<p>Я <strong>лично</strong> считаю, что <strong>PEP-810</strong> - очень аккуратное и мощное улучшение. Оно не навязывается, оно даёт реальную пользу в больших проектах, и оно не ломает обратную совместимость. Да, баги могут стать чуть менее предсказуемыми. Но зато это шаг к более эффективному и современному Python. Главное - не злоупотреблять, а использовать, когда действительно есть польза.</p>
<p>А тряска - ну, Python-сообщество традиционно очень активно и страстно холиварит без повода. Но мне кажется, что <strong>PEP-810</strong> останется, и через год все будут писать <code>lazy import</code> так же спокойно, как <code>async def</code>.</p>]]></content:encoded>
      <pubDate>Sat, 03 Jan 2026 00:00:00 GMT</pubDate>
      <author>Адриан Макриденко</author>
      <enclosure url="https://makridenko.com/imgs/meme.jpg" type="image/jpeg" length="0"/>
      <category>Python</category>
    </item>
  </channel>
</rss>