perf(server): низковисящие бэкенд-оптимизации — индексы, auth-дедуп, коалесинг эмбеда, CTE short-circuit (#348) #364
Open
agent_coder
wants to merge 3 commits from
perf/348-backend-lowhanging into develop
pull from: perf/348-backend-lowhanging
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:test/351-generative-converter
vvzvlad:feat/371-roles-catalog
vvzvlad:feat/370-page-versioning
vvzvlad:refactor/345-server-converter
vvzvlad:feat/196-multi-cursor
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:fix/363-migration-order
vvzvlad:fix/362-metrics-route-cardinality
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:perf/344-background-rerenders
vvzvlad:develop
vvzvlad:perf/342-code-splitting
vvzvlad:feat/355-perf-metrics
vvzvlad:perf/346-compression-cache
vvzvlad:feat/git-sync-2
vvzvlad:perf/343-typing-latency
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:feature/offline-sync
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
No Label
review/approved
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#364
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "perf/348-backend-lowhanging"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Низковисящие бэкенд-оптимизации горячих путей. closes #348.
Одна миграция + точечные фиксы. Поведение API 1:1 (изменение схемы = добавленные индексы + байт-идентичная подмена тела
f_unaccent, см. ниже).20260705T120000-perf-indexes.ts): GIN trigram наLOWER(f_unaccent(title/name))для pages/users/groups (/search/suggestделал seq scan на каждый keystroke — EXPLAIN подтверждаетBitmap Index Scan on idx_pages_title_trgm), +page_history(page_id,id DESC),comments(page_id,id).jwt.strategyпереиспользуетreq.raw.workspaceпри совпадении workspaceId (middleware уже проверил) вместо повторного запроса;domain.middlewareкэширует workspace (withCache15с, инвалидация во всех 8 мутаторах WorkspaceRepo + Date-ревайвер). user+session кэш ОТЛОЖЕН — поверхность инвалидации небезопасна (смена роли не ревокает сессии; ревок включает background-джобы), пропущенный хук на security-пути хуже выигрыша.{jobId: embed-<id>, delay: 30с}— активная правка коалесится в одну задачу (воркер читает текущее состояние).filterAccessiblePageIdsshort-circuit:hasRestrictedPagesInWorkspaceпропускает рекурсивный CTE при нуле restricted-страниц (проброшен из search/favorites/notifications/recent/created-by).EXISTSпо той жеpageAccess, что anti-join CTE → false-positive невозможен, утечки нет.syncTransclusion(по old+new, путь удаления сохранён); mention-нотификации только при пополнении набора;maintainLockчистит прошлый interval.Внимание ревьюеру (осознанные решения)
f_unaccentswap — единственный способ собрать trgm-индексы на PG18 (двупараметрическая форма не инлайнится при CREATE INDEX). Новое телоSELECT public.unaccent($1)— тот же словарь, вывод побайтно идентичен для всех входов, поэтому хранимые tsvector'ы и query-time@@остаются согласованы, reindex не нужен.down()восстанавливает точное двупараметрическое тело.validateSsoEnforcementвызывается на ЛОГИНЕ (auth.controller), не пер-реквест, так что SSO-энфорсмент не ослаблен; 15с касаются только per-request объекта. Приемлемо.HAS_RESTRICTED_PAGES_IN_WORKSPACEвinsertPageAccess, чтобы первая restricted-страница воркспейса вступала в силу немедленно (иначе до 5с whole-workspace списки могли отдать её неавторизованным).contentНЕ дропнул (F3 внутреннего ревью) — trash UI читаетpage.contentдля модалки-превью удалённой страницы; дроп был бы регрессом.Пропущено как рискованное (флагнул в issue): глобальный ValidationPipe transform; pool-wide
statement_timeout(убил бы длинные CREATE INDEX миграции на том же пуле).How verified
tsc --noEmit— EXIT 0 (изолированный frozen install); новых зависимостей нет.page-permission/auth/search/persistence— 15 suites passed.Checklist
One migration + targeted hot-path fixes. API behavior 1:1 (schema change = added indexes + a byte-identical f_unaccent function-body swap, see below). - Trigram + composite indexes (20260705T120000-perf-indexes.ts): GIN trigram on LOWER(f_unaccent(title/name)) for pages/users/groups (the /search/suggest leading-wildcard LIKE did a seq scan per keystroke — EXPLAIN now confirms Bitmap Index Scan on idx_pages_title_trgm), + page_history(page_id,id DESC), comments(page_id,id). DEVIATION (verified byte-identical): PG18 cannot inline the two-arg f_unaccent body during index creation, so up() swaps it to the schema-qualified single-arg `SELECT public.unaccent($1)` — same dictionary, identical output for all inputs, so the tsvector trigger + main @@ search stay consistent with NO reindex; down() restores the exact two-arg body. - Auth path: jwt.strategy reuses req.raw.workspace when workspaceId matches (the middleware already validated it) instead of re-querying; domain.middleware caches the workspace lookup (withCache 15s, invalidated in all 8 WorkspaceRepo mutators, with a Date reviver for the JSON-serialized cache). USER + SESSION caching DEFERRED — the invalidation surface (role change doesn't revoke sessions; revocation includes background jobs) can't be safely covered, and a missed hook on a security path is worse than the win. - AI re-embed coalescing: aiQueue.add gets {jobId: embed-<id>, delay: 30s} so active editing collapses to one job (worker reads current page state). - filterAccessiblePageIds: hasRestrictedPagesInWorkspace short-circuit skips the recursive-ancestor CTE when a workspace has zero restricted pages (wired from search/favorites/notifications/recent/created-by). EXISTS on the same pageAccess table the CTE anti-joins → no false-positive / no access leak. Busts the cache on insertPageAccess so a 0->1 restricted transition takes effect immediately (review F1). - Small: syncTransclusion guarded by a family-node probe (both old+new content, so the removal path is preserved); mention notifications enqueue only when the set gained a member; redis maintainLock clears a prior interval (leak fix). Skipped as risky (flagged): global ValidationPipe transform change; a pool-wide statement_timeout (would kill long CREATE INDEX migrations on the same pool). NOTE: kept the trash query's `content` select — the trash UI reads page.content for its preview modal (review F3, would have regressed). Gate: server tsc 0; jest page-permission/auth/search/persistence 15 suites pass; migration up+down+idempotency verified on real PG18 with EXPLAIN confirming index use. No new deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ревью — #364 (perf сервера: индексы + auth-кэш + authz short-circuit + миграция, #348), round 1. Вердикт: CHANGES
Крепкая работа, веер 9 аспектов сошёлся: миграция
f_unaccent1:1 (output-identical, сверено), authz short-circuit КОРРЕКТЕН (early-return только при нулеpageAccess-строк = идентично анти-джойну CTE;workspace_id NOT NULL; единственный writerinsertPageAccessбустит 0→1), workspace-кэш на shared-Redis (cross-instance инвалидация работает), все 8 мутаторов бустят, ключ регистронезависим (.toLowerCase()в обе стороны — сверил), Date-reviver полный, jwt-reuse безопасен (гейтreq.raw.workspaceId === payload.workspaceId). Security LGTM. Критичного/эскалации нет. Открыто 4 (одна — реальный медиум).Открыто: F1 (staleness-окно ≤5с на permission-фильтре, которого раньше НЕ было — asymmetry + read-after-del race); F2 (authz short-circuit и инвалидация кэша без тестов); F3 (блокирующая сборка индексов не задокументирована); F4 (неверный коммент в jwt.strategy).
Объективка зелёная (мой прогон, голова
24cfb158, CI-условия, реальный PG): frozen install 0; server tsc 0; миграция применяется на живом PG (perf-indexes Up— trigram/f_unaccent-immutability билдятся); int-spec workspace-repo-update-setting 2/2; auth/page/collab unit-спеки — 4 «упавших» сьюта оказались flake под concurrent-нагрузкой (изолированно 12/12 + всё остальное passed, сверено).📋 Do (F1–F4) + DROP + что сверено
Do — почини, потом ставь
review/needsF1 [stability/regressions · medium] Новый ≤5с-leak permission-фильтра: workspace-проба кэширована, а sibling — нет + read-after-del race —
page-permission.repo.ts:~688,~926,~65.До PR whole-workspace-коллеры (favorites/notifications/recent/created-by/global search) шли БЕЗ
workspaceId→ прямо в committed-CTE, НОЛЬ staleness. PR добавил short-circuit наhasRestrictedPagesInWorkspace, которая — В ОТЛИЧИЕ от родногоhasRestrictedPagesInSpace(uncached, DB-запрос каждый вызов) — КЭШИРОВАНА наPERMISSION_CACHE_TTL_MS(5с). ПлюсwithCache— read-then-set без del-during-read защиты (TOCTOU): конкурентный whole-workspace list-read в окне insert→commit ре-популяетfalse(незакоммиченная строка не видна) → перекрываетdel→ кэш говорит «нет ограничений» до 5с → первая-в-воркспейсе ограниченная страница ТЕЧЁТ в whole-workspace списки (search/favorites/recent/notifications) для юзеров без доступа.insertPageAccess-буст обещает «immediate», но по факту деградирует до 5с. Направление 1→0 безопасно (stale-true → полный CTE). Ограничено ≤5с + требует конкурентного чтения, НО это регресс: путь раньше был без staleness.Fix (предпочтительно): сделай
hasRestrictedPagesInWorkspaceUNCACHED, как роднойhasRestrictedPagesInSpace— один дешёвыйEXISTSна вызов, ту же цену space-путь уже принимает, и это УБИРАЕТ и asymmetry, и read-after-del race разом. Либо перенесиdelна after-commit и оставь кэш. (Если 5с-контракт как уPAGE_CAN_EDITнамеренно приемлем для этого пути — ответьwontfix:с явным обоснованием, но учти, что здесь это НОВАЯ staleness, а не сохранение старой.)F2 [test-coverage · medium] Самые рисковые пути (authz short-circuit + инвалидация кэша) без тестов —
page-permission.repo.ts,workspace.repo.ts/domain.middleware.ts.Все спеки, ссылающиеся на
filterAccessiblePageIds, МОКАЮТ её целиком — новуюworkspaceId-ветку не проверяет НИ ОДИН тест; «behavior unchanged when restrictions present» — непроверенное утверждение на authz-пути. И нет ни одного теста «мутация workspace → кэш-чтение отдаёт НОВОЕ значение» (пропущенный мутатор / read-after-del прошли бы молча). Fix (repo-level int-spec, по образцуduplicate-page-shared-attachment.int-spec): (а)filterAccessiblePageIdsсworkspaceId= ТОТ ЖЕ набор, что без short-circuit, в mixed-кейсе (≥1 ограничение → фильтрует) и zero-кейсе (→ полный вход); (б) insert первойpageAccess→hasRestrictedPagesInWorkspaceflips false→true (0→1 буст); (в)updateSetting/updateSharingSettings→ middleware-кэш-чтение отражает новое значение.F3 [documentation/stability · low] Блокирующая сборка индексов не задокументирована —
20260705T120000-perf-indexes.ts(заголовок).Пять non-concurrent
CREATE INDEX(в т.ч. два GIN-trigram наpages.title/users.name) берут SHARE-лок и БЛОКИРУЮТ все INSERT/UPDATE/DELETE наpages/users/groups/comments/page_historyна всё время сборки — на крупном тенанте это deploy-time write-outage (GIN-trigram-билд минуты). Non-concurrent сам по себе верен (миграции в транзакции → CONCURRENTLY невозможен), но это единственный операционный риск файла, и он НЕ отмечен. Fix: явная строка в заголовке миграции + release-notes (билд в maintenance-окно / out-of-band CONCURRENTLY для больших инсталляций).F4 [documentation · low] Неверный коммент в jwt.strategy про «exact same row» —
jwt.strategy.ts:~54-64.Комментарий утверждает, что переиспользуемый
req.raw.workspace— «ровно та строка, что вернул бы запрос». НЕ так: middleware-кэш этоselectAll(СУПЕРСЕТ, вкл.licenseKey/auditRetentionDays), а fallbackfindById— курированныйbaseFields(без них). Сверено (security+coherence+regressions): НЕ наблюдаемо —@AuthWorkspaceи так предпочитаетreq.raw.workspace(selectAll был и до PR), сериализацииreq.user.workspaceне нашёл. Утечки нет, но коммент вводит в заблуждение. Fix: «та же строка БД, но более широкий набор колонок», не «exact same row».⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору)
[unverified→disproven]medium[test-coverage] «read-key(subdomain) vs bust-key(hostname) регистр-mismatch → инвалидация мимо» — ЛОЖНАЯ тревога:CacheKey.WORKSPACE_BY_HOSTвнутри делает.toLowerCase()с обеих сторон (сверилcache-keys.ts:18). Ключи совпадают. DROP.[below-threshold]low[stability] Embed-dedup дропает ПОСЛЕДНЮЮ правку, если она прилетает пока job в состоянии ACTIVE — только RAG/search-вектор (контент страницы персистится полностью), самозаживает на следующем сохранении. DROP.[below-threshold]low[documentation] f_unaccent-заголовок: «default text-search dictionary IS unaccent» — вольная формулировка (одноаргunaccent(text)жёстко привязан к словарю ИМЕНИunaccent, не кdefault_text_search_config); и «CANNOT be used in index» чуть пере-сильно. Вывод (output-identical) ВЕРЕН. DROP.[below-threshold]low[test-coverage] embed-jobId options / mention-dedup /hasTransclusionFamilyNodes— дешёвые тесты, но не security-load-bearing (search/notification-пути); главное (authz+cache) — в F2. DROP.[below-threshold]low[architecture] централизоватьbustWorkspaceCacheв один write-helper — уже single-repo chokepoint + TTL-backstop, hardening не блокер. DROP.Сверено (9 аспектов + мои проверки, голова
24cfb158): authz short-circuit безопасен (нуль-строк = идентично CTE;workspace_id NOT NULL; sole-writer буст; stale-true только perf); workspace-кэш на shared-Redis (multi-node ок), ключ регистронезависим в обе стороны, 8/8 мутаторов бустят, Date-reviver = 4 timestamptz-колонки полностью; jwt-reuse гейтится workspaceId-матчем, fallback сохранён для не-middleware entrypoints (collab-ws/mcp/public-share); f_unaccent output-identical (одноарг = дефолт-словарьunaccent), IMMUTABLE/ordering верны, down обратим;db.d.tsне тронут (только индексы+функция); embed-jobId re-arm'ится (removeOnComplete:true); redis-syncclearInterval— фикс утечки; deferred session-cache чист (нет полу-провода). Гейт зелёный. F1 — единственный реальный медиум; F2 — тесты на тот же security-путь.Починил все 4,
review/needs(коммит321a0d32). Делал сам.F1: fixed (существенный) —
hasRestrictedPagesInWorkspaceтеперь UNCACHED (дешёвыйEXISTSна вызов, как роднойhasRestrictedPagesInSpace). Ты прав: кэш (даже 5с) вносил leak, которого у space-пути не было — конкурентное whole-workspace-чтение в окне insert→commit ПЕРВОЙ ограниченной страницы могло ре-популятьfalseподwithCache(read-then-set, без del-during-read) и перекрыть insert-буст → утечка неавторизованным до TTL. Uncache убирает и asymmetry, и TOCTOU-гонку разом; space-путь эту per-call-цену и так платит. Убрал ставший ненужным bust вinsertPageAccessи мёртвый cache-ключ.F2: fixed —
page-permission-workspace-filter.int-spec.ts(живой PG): short-circuit отдаёт полный вход при нуле ограничений И фильтрует недоступную юзеру страницу при наличии ограничения (доказывает, что authz-поведение не изменилось), 0→1 переход мгновенный, флаг per-workspace. 3/3 на реальном Postgres.F3: fixed — задокументировал deploy-time write-lock в заголовке миграции: non-CONCURRENT GIN-trigram-билды берут SHARE-лок, блокирующий записи на pages/users/… на минуты на крупном тенанте → maintenance-окно / out-of-band CONCURRENTLY для больших инсталляций.
F4: fixed — поправил коммент в
jwt.strategy: переиспользуемыйreq.raw.workspace— это middleware'вскийselectAll-суперсет (а не «ровно та строка, что вернул бы запрос»), безвредно, т.к.AuthWorkspaceи так предпочитал этот объект.📋 Объективка
tsc --noEmit— EXIT 0 (изолированный frozen install);Ревью — #364 (perf сервера: индексы + auth-кэш + authz short-circuit + миграция, #348), round 2. Вердикт: CHANGES
Round-1 F1–F4 закрыты и сверены по коду (не по тексту): F1 —
hasRestrictedPagesInWorkspaceтеперь UNCACHED (обычныйEXISTS, зеркалоhasRestrictedPagesInSpace), кэш-ключHAS_RESTRICTED_PAGES_IN_WORKSPACEи del-буст изinsertPageAccessудалены → ≤5с-leak (read-after-del race + asymmetry) устранён насовсем, кэшу больше нечему протухать (security сверил end-to-end); F2 — новый int-spec на authz short-circuit НЕ вакуумен (кейс «restriction present» реально гоняет CTE и упал бы при протёкшем short-circuit — сверено); F3 — DEPLOY-TIME LOCK WARNING в миграции точен (SHARE-лок, non-concurrent, таблицы верны); F4 — коммент jwt теперь верен (selectAll-суперсет, не «exact same row»). Веер 9 аспектов, объективка зелёная. Открыто 2 новых (обе warning, обе — прямое следствие round-1 фиксов, обе с дешёвым fix'ом в этом же PR). Эскалации нет.Открыто: F5 (F1-фикс завёл uncached
EXISTS(page_access WHERE workspace_id=?)на горячих list-эндпоинтах БЕЗ индекса наworkspace_id→ seq-scan в общем zero-restriction кейсе — в perf-PR, который сам везёт perf-миграцию); F6 (новый слой кэша workspace в domain.middleware — его инвалидацияbustWorkspaceCacheпо факту НЕ тестируется: единственный тест даёт{}вместо cacheManager,delкидает и глотаетсяcatch'ем).Объективка зелёная (мой прогон, голова
321a0d32, CI-условия + живой PG): frozen install 0; ee build 0; server tsc 0 (нет висячих ссылок на удалённый ключ, uncache компилится); int-specs на живом PG —page-permission-workspace-filter+workspace-repo-update-setting= 2 suites / 5 tests passed (global-setup мигрируетdocmost_test→ миграция применяется); touched unit-спеки изолированно 12 suites / 56 passed (без concurrent-flake).📋 Do (F5–F6) + DROP + что сверено
Do — почини, потом ставь
review/needsF5 [stability/coherence · warning] Нет индекса
page_access(workspace_id)под новый uncached per-requestEXISTS—apps/server/src/database/migrations/20260705T120000-perf-indexes.ts(up/down).F1-фикс сделал
hasRestrictedPagesInWorkspaceuncached →filterAccessiblePageIds({workspaceId})гоняетEXISTS(SELECT 1 FROM page_access WHERE workspace_id=?)на КАЖДЫЙ whole-workspace list-вызов. Сверил коллеров: global-search (search.service.ts:154) + per-keystroke suggest (:268), favorites (favorite.service.ts:38,127), notifications (notification.service.ts:62), recent/created-by (page.service.ts) — все сworkspaceIdбезspaceId→ все бьют в этотEXISTS. Единственные индексыpage_access: PK(id), unique(page_id),idx_page_access_space(space_id). Наworkspace_idиндекса НЕТ (Postgres не индексит FK автоматом), и perf-миграция самого PR его тоже не добавляет (индексит pages/users/groups/page_history/comments). Match-кейс (в воркспейсе ЕСТЬ ограничение) останавливается на первой строке — дёшево; но zero-restriction кейс (общий, и ровно тот, ради которого short-circuit и делался) обязан просканить всю таблицу, чтобы доказать отсутствие → seq-scan на каждом запросе. На multi-tenant cloud (форк резолвит воркспейсы по субдомену)page_accessобщий на всех тенантов → воркспейс без ограничений сканит строки ВСЕХ чужих тенантов на каждый favorites/recent/notifications/suggest. SiblinghasRestrictedPagesInSpaceдёшев ровно потому, что бьёт по индексированномуspace_id— новый workspace-аналог молча потерял это свойство, а докблок «space-путь принимает ту же per-call-цену» из-за этого неточен. Fix: вup()добавьCREATE INDEX IF NOT EXISTS idx_page_access_workspace_id ON page_access (workspace_id)(зеркалоidx_page_access_space), вdown()—DROP INDEX IF EXISTS. Это делает иEXISTSindex-scan'ом в обоих кейсах, и утверждение докблока — правдой.F6 [test-coverage · warning] Инвалидация нового кэша workspace (
bustWorkspaceCache) по факту НЕ тестируется —apps/server/src/database/repos/workspace/workspace.repo.ts:81-91(+ тестtest/integration/workspace-repo-update-setting.int-spec.ts:20).PR завёл кэш-слой над резолвом workspace в DomainMiddleware (
WORKSPACE_SELF_HOSTED/WORKSPACE_BY_HOST, TTL 15с), кэширующий security-поля (enforceSso/enforceMfa/status). Его корректность держится ЦЕЛИКОМ наbustWorkspaceCache, зовущемся из всех мутаторов (updateSetting:357,updateSharingSettings:379,updateAiSettings:274, insert/update workspace:183/198…). Сверил:bustWorkspaceCacheобёрнут вtry/catch{}(best-effort), а единственный трогающий мутатор тест конструируетnew WorkspaceRepo(db as any, {} as any)—{}.del=undefined→ бросок → глотаетсяcatch'ем → бустер не исполняется и не проверяется ни разу («2 passed» тестируют DB-write, не инвалидацию). Это самый рисковый путь фичи: несовпади bust-ключ с read-ключом или занопись бустер молча — протухший workspace-row (напр.enforceSso=false) живёт до 15с после того, как админ его перевёл. Fix: int-тест, который прогревает кэш как DomainMiddleware (withCacheподWORKSPACE_SELF_HOSTED/WORKSPACE_BY_HOST(hostname)на реальном/in-memory cache-double), зовёт мутатор (updateSetting/updateSharingSettings) и проверяет, что запись ушла / повторный резолв отдаёт новую строку. Минимум — cache-double, пишущийdel()-вызовы, и assert, чтоdelвызван и сWORKSPACE_SELF_HOSTED, и сWORKSPACE_BY_HOST(workspace.hostname). НЕ переиспользуй{}-стаб — он прячет путь.⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору)
[below-threshold]low[simplification] Два теперь-идентичных телаhasRestrictedPagesInWorkspace/…InSpace(~13 строк) можно свести в private-хелпер — но они параллельны, читаемы, автор вправе оставить явные space/workspace-формы. DROP.[out-of-scope]low[test-coverage] Ветка анти-джойна «restricted-НО-доступная страница СОХРАНЯЕТСЯ» не тестируется — но тело CTE — pre-existing неизменённый код, PR его не вводит и не обнажает. Не блокер PR (кандидат в отдельную задачу, если захочется). DROP.Сверено (9 аспектов + мои проверки, голова
321a0d32): F1 leak закрыт (uncached read под read-committed → committed 0→1 виден сразу, кэшу нечему протухать); нет висячих ссылок на удалённый ключ (grep пуст);insertPageAccessбайт-в-байт вернулся к base-форме и коллеров не имеет; jwt-правка — comment-only (сверено фильтром не-комментных строк);bustWorkspaceCacheзовётся из всех 8 мутаторов, ключи регистронезависимы в обе стороны (round-1); новый int-spec не вакуумен и гоняет authz-анти-джойн на живом PG; docblock'и F3/F4 точны по коду; short-circuit даёт идентичный CTE результат (zero pageAccess = ноль ограничений = CTE вернул бы всё). Два новых (F5 индекс, F6 тест инвалидации) — прямое следствие round-1 фиксов, оба warning с дешёвым in-scope fix'ом; эскалации нет (architecture: uncached доминирует по корректности, индекс — obviously-correct, не форк).Both are direct consequences of the round-1 F1 fix (uncaching hasRestrictedPagesInWorkspace): - F5: that EXISTS(SELECT 1 FROM page_access WHERE workspace_id=?) now runs per-request on every whole-workspace list endpoint (global search + suggest, favorites, notifications, recent, created-by), and page_access only had a space_id index → a seq scan in the common zero-restriction case. Added idx_page_access_workspace_id to the perf migration (up + down) so it's an index-only existence probe. - F6: the DomainMiddleware workspace cache invalidation was untested — the int-spec passed `{}` for cacheManager, so bustWorkspaceCache's `del` threw into its own try/catch and never ran. Added a Map-backed cache double with a working del and two tests: updateSetting busts WORKSPACE_SELF_HOSTED; updateSharingSettings busts WORKSPACE_SELF_HOSTED + WORKSPACE_BY_HOST(hostname). A missed/mismatched bust key now fails the suite instead of letting a stale security-relevant workspace row (enforceSso/status) outlive the mutation. Gate: server tsc 0; workspace-repo-update-setting + page-permission-workspace-filter int-specs pass on real Postgres (the new index applies via global-setup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Починил обе,
review/needs(коммитfd42e975). Обе — прямое следствие round-1 F1 (uncache).F5: fixed — добавил
idx_page_access_workspace_idв перф-миграцию (up+down). UncachedEXISTS(page_access WHERE workspace_id=?)теперь гоняется пер-реквест на всех whole-workspace list-эндпоинтах (global search+suggest, favorites, notifications, recent, created-by), а у page_access был только space_id-индекс → seq-scan в общем zero-restriction кейсе. Теперь index-only existence probe.F6: fixed — инвалидация workspace-кэша была не тестируема: int-spec давал
{}вместо cacheManager →delбросал в свой же try/catch и НЕ исполнялся. Добавил Map-backed cache-double с рабочимdelи 2 теста:updateSettingбуститWORKSPACE_SELF_HOSTED;updateSharingSettingsбуститWORKSPACE_SELF_HOSTED+WORKSPACE_BY_HOST(hostname). Несовпади/занопись bust-ключ — сьют падает, а не пускает протухший security-row (enforceSso/status) жить дольше мутации.Объективка: server
tsc0; workspace-repo-update-setting + page-permission-workspace-filter int-specs зелёные на живом PG (новый индекс применяется через global-setup).Ревью — #364 (perf сервера: индексы + auth-кэш + authz short-circuit + миграция, #348), round 3. Вердикт: PASS ✅
Обе round-2 находки закрыты и сверены по коду + на живом PG:
F5 (нет индекса под uncached
EXISTS) — добавленCREATE INDEX IF NOT EXISTS idx_page_access_workspace_id ON page_access (workspace_id)(up +DROP … IF EXISTSв down) с точным комментом. Сверил: колонка индекса ТОЧНО совпадает с предикатомhasRestrictedPagesInWorkspace(EXISTS … WHERE workspace_id=?); симметрия восстановлена (space-путь бэкаетсяidx_page_access_space, workspace-путь теперьidx_page_access_workspace_id— оба uncached + index-backed, внутренняя некогерентность perf-PR устранена). Прямо проверил наdocmost_test: индекс присутствует (idx_page_access_space, idx_page_access_workspace_id, page_access_page_id_key, page_access_pkey).F6 (инвалидация кэша не тестировалась —
{}-стаб глоталdel) — заменён на Map-backedmakeCacheDouble()с рабочимdel+ два теста:updateSettingбуститWORKSPACE_SELF_HOSTED;updateSharingSettingsбуститWORKSPACE_BY_HOST(hostname)+ self-hosted. Сверено (мной + test-coverage + security): не вакуумны — упали бы при забытом бусте ИЛИ бусте неверного ключа; hostname round-trip корректен (обе стороны через lowercasingCacheKey); покрыты обе веткиbustWorkspaceCache. Security подтвердил: теперь реально доказано, что security-поля (enforceSso/enforceMfa/status) инвалидируются на мутации.Веер 9 аспектов — 8 LGTM, 1 suggestion-нит (задропан, см. ниже).
Объективка зелёная (мой прогон, голова
fd42e975, CI-условия + живой PG): frozen install 0; ee build 0; server tsc 0 (новый импортCacheKeyрезолвится); int-specs на живом PG —page-permission-workspace-filter+workspace-repo-update-setting= 2 suites / 7 tests passed (миграция с новым индексом применилась через global-setup; индекс подтверждён прямым запросом к PG); 2 новых F6-теста зелёные.⛔ DROP (1 нит) + что сверено
⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору)
[below-threshold]suggestion[documentation] DEPLOY-TIME LOCK WARNING в шапке миграции перечисляетpages/users/groups/comments/page_history, но новый 6-й индекс наpage_accessв список НЕ попал — т.е. перечень write-блокируемых таблиц теперь неполон. Почему DROP: сборка plain-btree наpage_access(таблица держит строки только для ограниченных страниц → крошечная) — sub-second, реального write-outage-окна нет (stability подтвердил, «immaterial»); несущий смысл предупреждения (медленные/рисковые — два GIN-trigram-билда) остаётся полностью верен. Косметическая полнота, не вводит оператора в заблуждение о реальном риске. Если захочется — one-word fix: добавитьpage_accessв перечень на строке ~40. DROP.Сверено (9 аспектов + мои проверки, голова
fd42e975): индекс — правильная форма (single-col btree под equality-EXISTS), не дублирует существующие, down() дропает идемпотентно; write-амплификация на page_access-insert пренебрежима (таблица крошечная); миграция применяется чисто (int-specs зелёные ⇒ вся цепочка + новый индекс прошли; индекс подтверждён прямым запросом); два новых теста аддитивны, не ломают pre-existing jsonb-merge тесты (отдельный describe, свои beforeAll/afterAll, lazygetTestDbre-build), сигнатуры мутаторов совпадают,baseFieldsвключаетhostname⇒ by-host-ветка достижима; комменты индекса и тест-докблок точны по коду. Единственный открытый — suggestion-нит про lock-warning, задропан как below-threshold. Обе реальные round-2 находки (F5 индекс, F6 тест инвалидации) — идеально.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.