vault backup: 2026-04-29 21:58:01

This commit is contained in:
Vadym Samoilenko 2026-04-29 21:58:01 +01:00
parent 0e64b4727e
commit d7af8d73a7
14 changed files with 586 additions and 10 deletions

View file

@ -4,7 +4,7 @@
"syncFolder": "Hoarder",
"attachmentsFolder": "Hoarder/attachments",
"syncIntervalMinutes": 60,
"lastSyncTimestamp": 1777492621168,
"lastSyncTimestamp": 1777496221546,
"updateExistingFiles": false,
"excludeArchived": true,
"onlyFavorites": false,

View file

@ -0,0 +1,35 @@
---
bookmark_id: "gknfa711l5vpiogq1gbh1pdz"
url: |
https://v23.astar.bz/10838-fermerskaya-zhizn-v-inom-mire-2-sezon-isekai-nonbiri-nouka-2.html
title: |
Фермерская жизнь в ином мире 2 сезон , Isekai Nonbiri Nouka 2 смотреть онлайн или скачать бесплатно
date: 2026-04-29T20:17:43.000Z
modified: 2026-04-29T20:18:01.000Z
tags:
- Isekai
- Fantasy
- Anime
- Adventure
- Slice-Of-Life
note:
original_note:
summary:
banner: "[[Hoarder/attachments/924ae99c-fd3b-483f-a288-adee63415766-Фермерская-жизнь-в-ином-мире.jpg]]"
screenshot: "[[Hoarder/attachments/1de8d684-8ce7-4191-809e-1557a73b10ec-Фермерская-жизнь-в-ином-мире.jpg]]"
---
# Фермерская жизнь в ином мире 2 сезон , Isekai Nonbiri Nouka 2 смотреть онлайн или скачать бесплатно
![Фермерская жизнь в ином мире 2 сезон , Isekai Nonbiri Nouka 2 смотреть онлайн или скачать бесплатно - Banner Image](Hoarder/attachments/924ae99c-fd3b-483f-a288-adee63415766-Фермерская-жизнь-в-ином-мире.jpg)
![Фермерская жизнь в ином мире 2 сезон , Isekai Nonbiri Nouka 2 смотреть онлайн или скачать бесплатно - Screenshot](Hoarder/attachments/1de8d684-8ce7-4191-809e-1557a73b10ec-Фермерская-жизнь-в-ином-мире.jpg)
## Notes
[Visit Link](https://v23.astar.bz/10838-fermerskaya-zhizn-v-inom-mire-2-sezon-isekai-nonbiri-nouka-2.html)
[View in Hoarder](https://links.ai-impress.com/dashboard/preview/gknfa711l5vpiogq1gbh1pdz)

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View file

@ -21,9 +21,9 @@ This 3-hop pattern works for hundreds of articles without vector search.
| [[wiki/obsidian-rag/_index\|obsidian-rag/]] | Karpathy's LLM wiki method — Obsidian RAG, setup, vs true RAG | 3 |
| [[wiki/projects-overview/_index\|projects-overview/]] | All 42 Oliver Agency projects — grouped by server (optical-web-1, optical-dev, baic, box-cli) | 1 |
| [[wiki/tech-patterns/_index\|tech-patterns/]] | Recurring tech stacks: FastAPI, React/Vite, Next.js, Azure AD, AI, Box, One2Edit, Redis/Celery, cost-tracker | 15 |
| [[wiki/architecture/_index\|architecture/]] | Cross-cutting architectural patterns: Docker Compose, multi-agent AI, GCP timeout, RAG, hotfolder, optical-dev deploy, cost-tracker, new-project checklist, troubleshooting playbooks, ADR log | 10 |
| [[wiki/architecture/_index\|architecture/]] | Cross-cutting architectural patterns: Docker Compose, multi-agent AI, GCP timeout, RAG, hotfolder, optical-dev deploy, cost-tracker, new-project checklist, troubleshooting playbooks, ADR log, Cloud Run Jobs | 11 |
| [[wiki/client-knowledge/_index\|client-knowledge/]] | Per-client notes for Ford, H&M, L'Oréal, Barclays, Ferrero, 3M | 6 |
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 69 |
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 73 |
| [[wiki/connections/_index\|connections/]] | Cross-cutting insights linking 2+ concepts: FastAPI+Azure AD+Docker trinity, AI→cost-tracker, Apache+Vite basePath, GCP→REST polling, Box+hotfolder, Docker DNS+AdGuard | 9 |
| [[wiki/qa/_index\|qa/]] | Filed answers to queries (saved with `--file-back`) | 0 |
| [[wiki/homelab/_index\|homelab/]] | Self-hosted infra: Proxmox install, IOMMU/PCI passthrough, hypervisor setup, budget builds, HP Elitedesk G3, Homarr API + Apps + Boards + Certificates + Integrations + Settings + Tasks + AdGuard + Clock + Docker Stats + Docker Integration + Download Client + Firewall + Proxmox Integration + Radarr + Readarr + Sonarr + Bookmarks + Calendar + Icons + App Widget + Weather + GitHub + Nextcloud + qBittorrent + RSS Feed + Speedtest Tracker + System Health Monitoring + System Resources + Services Map + Media Stack | 39 |

View file

@ -3,7 +3,7 @@ title: "Architecture Patterns Index"
description: "Cross-cutting architectural decisions across Oliver Agency projects"
tags: [index, architecture]
created: 2026-04-15
updated: 2026-04-27
updated: 2026-04-29
---
# Architecture Patterns
@ -24,6 +24,7 @@ Cross-cutting architectural decisions that appear in multiple Oliver projects.
| [[wiki/architecture/new-project-checklist\|new-project-checklist]] | Step-by-step Oliver project setup — repo, Docker Compose, Azure AD, cost tracker, optical-dev deploy | All new projects |
| [[wiki/architecture/troubleshooting-playbooks\|troubleshooting-playbooks]] | Failure → diagnosis → fix for FastAPI, Docker, React/Vite, Azure AD, Apache, PostgreSQL | All Oliver projects |
| [[wiki/architecture/adr-log\|adr-log]] | Architecture Decision Records — why HTTP polling, Docker Compose, FastAPI, Azure AD, cost tracker were chosen | All Oliver projects |
| [[wiki/architecture/cloud-run-jobs-celery\|cloud-run-jobs-celery]] | Moving heavy Celery tasks (ffmpeg, TTS, Whisper) to Cloud Run Jobs — finite execution, pay-per-use, env-specific compose, chain dispatch pattern | Video Accessibility |
## Key Architectural Decisions

View file

@ -0,0 +1,112 @@
---
title: "Cloud Run Jobs for Heavy Celery Tasks"
description: "Moving compute-heavy async tasks (ffmpeg, TTS, Whisper) from Celery workers to Google Cloud Run Jobs"
tags: [architecture, gcp, cloud-run, celery, video-accessibility, workers]
created: 2026-04-29
updated: 2026-04-29
---
# Cloud Run Jobs for Heavy Celery Tasks
## Key Takeaways
- Cloud Run **Jobs** (not Services) are for finite tasks — pay only for execution time, no idle cost
- Heavy workers (`ffmpeg-worker`, `tts-worker`, `whisper-worker`) set to `replicas: 0` on environments that delegate to Cloud Run
- `USE_CELERY_FALLBACK=true` keeps `.delay()` working locally without Cloud Run
- `--redeploy` flag in deploy script = git pull + restart only (no rebuild) — critical for hotfixes
- Task chain (ingest→translate→render) must be dispatched from within the Cloud Run Job process itself — Celery callbacks don't work across the boundary
## Architecture: Celery → Cloud Run Jobs Migration
The Video Accessibility project (and similar heavy-AI pipelines) offloads:
| Task | From | To |
|------|------|----|
| `ingest_and_ai` | Celery `ai-queue` worker | Cloud Run Job |
| `translate_and_synthesize` | Celery `ai-queue` worker | Cloud Run Job |
| `render_accessible_video` | Celery `ai-queue` worker | Cloud Run Job |
Lightweight tasks (notifications, DB writes) stay on Celery `default` queue — see [[wiki/tech-patterns/redis-celery-worker-queue|redis-celery-worker-queue]].
## Environment-Specific Docker Compose
`docker-compose.optical-dev.yml` exists specifically because optical-dev has 2 CPUs and the prod compose had `cpus: '4.0'`:
```yaml
# docker-compose.optical-dev.yml
services:
ffmpeg-worker:
deploy:
replicas: 0 # delegated to Cloud Run Jobs
resources:
limits:
cpus: '1.0' # optical-dev only has 2 CPUs
tts-worker:
deploy:
replicas: 0
resources:
limits:
cpus: '1.0'
```
See [[wiki/concepts/docker-compose-cpu-limits-env|docker-compose-cpu-limits-env]] for the broader pattern.
## Cloud Run Job Env Vars
Use `^|^` as delimiter when values contain `=` (JWT secrets, base64 tokens):
```bash
gcloud run jobs update va-worker \
--set-env-vars "^|^KEY1=value1|KEY2=jwt=with=equals|KEY3=value3"
```
## Local Development Fallback
```env
USE_CELERY_FALLBACK=true
```
When this env var is set, `.delay()` calls route to local Celery workers instead of dispatching Cloud Run Jobs. Keeps local dev working without GCP credentials.
## Deploy Script Flags
```bash
./deploy.sh # full rebuild + deploy
./deploy.sh --redeploy # git pull + restart only (no docker build)
```
`--redeploy` is critical for hotfixes — build on GCP free-tier takes 1015 min queued + ~10 min build. Use it whenever the code change doesn't require image rebuild.
## Cloud Build Image Reference
```
europe-west1-docker.pkg.dev/{project}/nexus/va-worker:latest
```
## Task Chaining Constraint
When tasks run as Cloud Run Jobs, Celery callback chains don't work:
```python
# BROKEN: translate_and_synthesize won't fire after ingest completes
ingest_and_ai.delay(video_id).then(translate_and_synthesize.s())
# CORRECT: chain from within the Cloud Run Job process
def run_ingest_job(video_id):
result = ingest_and_ai(video_id)
if result.ok:
dispatch_cloud_run_job("translate_and_synthesize", video_id)
```
Alternative: use Cloud Workflows to orchestrate multi-step job chains.
## Related
- [[wiki/tech-patterns/redis-celery-worker-queue|redis-celery-worker-queue]] — Celery queue setup and AI queue separation
- [[wiki/concepts/docker-compose-cpu-limits-env|docker-compose-cpu-limits-env]] — why per-environment compose files are needed
- [[wiki/architecture/optical-dev-server-deploy|optical-dev-server-deploy]] — optical-dev server constraints
## Sources
- [[daily/2026-04-29.md]] — sessions 21:52 and 20:29 (Video Accessibility Cloud Run migration)

View file

@ -7,6 +7,9 @@ created: 2026-04-15
updated: 2026-04-29
---
> [!info] SSH Alias
> SSH into the Ford QC server using `box-cli` (→ 10.220.176.3). `box-cli-01` does NOT resolve.
# Ford
Oliver Agency works with Ford on asset pack automation — QC validation and delivery to Ford's GECHUB SFTP system.
@ -66,22 +69,49 @@ Oliver Agency works with Ford on asset pack automation — QC validation and del
> [!warning] Always verify working directory before deploying
> The systemd service for Ford QC runs from `ford_qc_git_dev/ford_qc/` — NOT from `FORD_ASSET_PACK_QC` or `FORD_ASSET_PACK_QC_NEW` (legacy directory names that still exist on disk).
**SSH:** `box-cli` (→ 10.220.176.3). Do NOT use `box-cli-01` — it does not resolve.
**Service names:**
- Dev: `ford-qc-hotfolder.service`
- Production: `ford-qc-hotfolder-PROD.service`
**To find the real working directory before deploying:**
```bash
systemctl show ford-qc --property=ExecStart
# Output: ExecStart={ path=/usr/bin/python3 ; argv[]=/usr/bin/python3 qc_engine.py ; ... WorkingDirectory=/home/oliver/ford_qc_git_dev/ford_qc ; ... }
systemctl show ford-qc-hotfolder-PROD --property=ExecStart
# or
systemctl cat ford-qc-hotfolder-PROD
# WorkingDirectory=/home/box-cli/FORD_SCRIPTS/ford_qc_git_dev/ford_qc
```
Never assume the working directory from directory names alone. Always use `systemctl show` to confirm.
Never assume the working directory from directory names alone. Always use `systemctl show` or `systemctl cat` to confirm.
**Known directories on the Ford QC server (do not confuse):**
| Directory | Status | Notes |
|-----------|--------|-------|
| `ford_qc_git_dev/ford_qc/` | **Active** | This is where the service runs |
| `FORD_ASSET_PACK_QC/` | Legacy | Old deployment, do not use |
| `FORD_ASSET_PACK_QC_NEW/` | Legacy | Abandoned migration attempt |
| `/home/box-cli/FORD_SCRIPTS/ford_qc_git_dev/ford_qc/` | **Active** | This is where the service runs |
| `/home/box-cli/FORD_SCRIPTS/FORD_ASSET_PACK_QC/` | Legacy | Old deployment, do not use |
| `/home/box-cli/FORD_SCRIPTS/FORD_ASSET_PACK_QC_NEW/` | Stale | Abandoned migration attempt |
**Git pull with local edits on server:**
```bash
git stash && git pull && git stash pop
```
**ford-gechub-sftp deploy path:** `/home/box-cli/FORD_SCRIPTS/ford-gechub-sftp` (different project)
## GPAS Zip Naming Convention
> [!important] Zip filename format changed
> Ford image packs must now end in `_GPAS.zip` — the old `_image.zip` suffix is no longer accepted.
This is enforced by `zip_filename_check.py`. Packs with incorrect suffixes fail QC immediately at the filename check stage. Update any scripts or templates that generate pack filenames.
| Old format | New format |
|-----------|-----------|
| `ford_pack_2026_image.zip` | `ford_pack_2026_GPAS.zip` |
## Ford BnP WERS Code Allowlist Pattern

View file

@ -79,5 +79,10 @@
| [[wiki/concepts/native-track-blob-url]] | Browsers block `data:text/vtt` URIs for `<track>` elements; must use `URL.createObjectURL(new Blob([vttString]))` and revoke in effect destructor | daily/2026-04-29.md | 2026-04-29 |
| [[wiki/concepts/claude-code-plugin-marketplace]] | `/plugin marketplace add <user>/<repo>` registers GitHub marketplace; then `/plugin install <name>@<marketplace-name>`; `/plugin add` without marketplace doesn't work for GitHub repos | daily/2026-04-29.md | 2026-04-29 |
| [[wiki/concepts/docker-compose-cpu-limits-env]] | `cpus: '4.0'` fails at `docker compose up` on a 2-CPU server — fix with per-environment compose override files | daily/2026-04-29.md | 2026-04-29 |
| [[wiki/concepts/etag-optimistic-locking]] | ETag/If-Match pattern for concurrent edit protection — backend hash, frontend state (not ref), 412 conflict handling | daily/2026-04-29.md | 2026-04-29 |
| [[wiki/concepts/double-submit-cookie-csrf]] | CSRF for stateless JWT APIs: csrf_token cookie + X-CSRF-Token header; every login path must set both cookies | daily/2026-04-29.md | 2026-04-29 |
| [[wiki/concepts/time-sleep-blocks-asyncio]] | `time.sleep()` inside async FastAPI handlers blocks the entire event loop — replace with `asyncio.sleep()` or `run_in_executor` | daily/2026-04-29.md | 2026-04-29 |
<!-- Articles added automatically by compile.py -->
<!-- Format: | [[concepts/slug]] | One-line summary | daily/YYYY-MM-DD.md | date | -->

View file

@ -0,0 +1,74 @@
---
title: "Docker Compose CPU Limits Break Per Environment"
aliases: [docker-cpu-limits, compose-cpu-env, cpus-range-error]
tags: [docker, docker-compose, deployment, devops, gotcha]
sources:
- "daily/2026-04-29.md"
created: 2026-04-29
updated: 2026-04-29
---
# Docker Compose CPU Limits Break Per Environment
Setting `cpus: '4.0'` in a prod `docker-compose.yml` and then running `docker compose up` on a server with only 2 CPUs causes a hard error. The fix is a per-environment compose override file that matches the target server's actual CPU count.
## Key Points
- Error message: `"range of CPUs is from 0.01 to 2.00"` — appears at `docker compose up`, NOT at `docker compose build`
- The build succeeds silently; the failure only surfaces at container startup
- prod compose is the canonical config; environment-specific files override resource limits only
- Naming convention: `docker-compose.{env}.yml` (e.g. `docker-compose.optical-dev.yml`)
## Details
The error occurs because Docker enforces CPU limits against the host's actual CPU count at container start time, not at image build time. A prod file with `cpus: '4.0'` is valid on a 4-core prod server but fails immediately on a 2-core staging server.
**Prod compose (canonical):**
```yaml
services:
ffmpeg-worker:
deploy:
resources:
limits:
cpus: '4.0' # valid on 4-core prod
memory: 8G
```
**optical-dev override:**
```yaml
# docker-compose.optical-dev.yml
services:
ffmpeg-worker:
deploy:
replicas: 0 # also disable heavy workers on this env
resources:
limits:
cpus: '1.0' # optical-dev has 2 CPUs — keep 1 for other services
memory: 2G
```
**Deploy command using override:**
```bash
docker compose -f docker-compose.yml -f docker-compose.optical-dev.yml up -d
```
## Pattern
| File | Role |
|------|------|
| `docker-compose.yml` | Canonical config — prod values, all services defined |
| `docker-compose.prod.yml` | Prod-specific overrides (rarely needed if base IS prod) |
| `docker-compose.optical-dev.yml` | optical-dev overrides — reduced CPU/RAM, some services disabled |
| `docker-compose.override.yml` | Local dev — picked up automatically by Docker Compose |
## Related Concepts
- [[wiki/architecture/cloud-run-jobs-celery|cloud-run-jobs-celery]] — why optical-dev uses this pattern for heavy workers
- [[wiki/architecture/optical-dev-server-deploy|optical-dev-server-deploy]] — optical-dev server constraints and deployment pattern
## Sources
- [[daily/2026-04-29.md]] — session 20:29, `cpus: '4.0'` breaking on 2-CPU optical-dev server

View file

@ -0,0 +1,114 @@
---
title: "Double Submit Cookie CSRF Pattern for JWT APIs"
aliases: [double-submit-cookie, csrf-jwt, csrf-stateless, x-csrf-token]
tags: [security, csrf, jwt, fastapi, auth, cookies]
sources:
- "daily/2026-04-29.md"
created: 2026-04-29
updated: 2026-04-29
---
# Double Submit Cookie CSRF Pattern for JWT APIs
For stateless JWT APIs, the traditional Synchronizer Token Pattern (server-side session stores token) is impossible. The Double Submit Cookie pattern provides CSRF protection without server state.
## Key Points
- `csrf_token` cookie is set alongside `refresh_token` at login (same `Set-Cookie` response)
- `/auth/refresh` validates that `X-CSRF-Token` request header matches the `csrf_token` cookie value
- The browser cannot read the cookie value cross-origin (SameSite + HttpOnly still blocks JS reads from other origins) — attacker can't forge the header
- **Critical:** EVERY code path that issues a `refresh_token` MUST also set `csrf_token` — missing even one (e.g. Microsoft OAuth callback) leaves users with refresh token but no CSRF cookie → silent 403s on next token refresh
## Details
### Why Not Synchronizer Token?
Synchronizer Token requires server-side session storage: server generates a token, stores it per-session, validates it on state-changing requests. Stateless JWT APIs have no session → impossible without adding Redis/DB session state.
### Double Submit Cookie Flow
```
POST /auth/login
← Set-Cookie: refresh_token=JWT...; HttpOnly; SameSite=Lax
← Set-Cookie: csrf_token=random_uuid; SameSite=Lax ← NOT HttpOnly (JS reads it)
POST /auth/refresh
→ Cookie: refresh_token=JWT...; csrf_token=abc123
→ X-CSRF-Token: abc123 ← JS reads cookie, puts in header
← 200 OK (tokens match)
# Cross-origin CSRF attack:
POST /auth/refresh (from evil.com)
→ Cookie: refresh_token=JWT... ← browser sends automatically
→ X-CSRF-Token: ??? ← attacker can't read the cookie → cannot forge
← 403 Forbidden
```
### FastAPI Implementation
```python
import secrets
@router.post("/auth/login")
async def login(response: Response, credentials: LoginRequest):
user = await authenticate(credentials)
refresh_token = create_refresh_token(user.id)
csrf_token = secrets.token_hex(32)
response.set_cookie("refresh_token", refresh_token, httponly=True, samesite="lax")
response.set_cookie("csrf_token", csrf_token, httponly=False, samesite="lax")
return {"access_token": create_access_token(user.id)}
@router.post("/auth/refresh")
async def refresh(
request: Request,
x_csrf_token: str | None = Header(None, alias="X-CSRF-Token"),
):
csrf_cookie = request.cookies.get("csrf_token")
if not x_csrf_token or x_csrf_token != csrf_cookie:
raise HTTPException(status_code=403, detail="CSRF validation failed")
# ... issue new access token
```
### All Login Paths Must Set CSRF
```python
# COMMON MISTAKE: Microsoft OAuth callback only sets refresh_token
@router.get("/auth/callback/microsoft")
async def ms_callback(code: str, response: Response):
user = await exchange_ms_code(code)
refresh_token = create_refresh_token(user.id)
response.set_cookie("refresh_token", refresh_token, httponly=True)
# ← MISSING: csrf_token not set here!
# Result: user logs in via MS → no csrf cookie → 403 on first /auth/refresh
```
The fix: extract a `set_auth_cookies(response, user_id)` helper that always sets both cookies, call it from every login path.
### Frontend
```typescript
// Read csrf_token from cookie (it's NOT httpOnly)
function getCsrfToken(): string | null {
return document.cookie
.split("; ")
.find(row => row.startsWith("csrf_token="))
?.split("=")[1] ?? null;
}
// Include in every refresh request
await fetch("/auth/refresh", {
method: "POST",
headers: { "X-CSRF-Token": getCsrfToken() ?? "" },
credentials: "include",
});
```
## Related Concepts
- [[wiki/concepts/etag-optimistic-locking|etag-optimistic-locking]] — another HTTP header-based protection pattern
- [[wiki/tech-patterns/azure-ad-msal-auth|azure-ad-msal-auth]] — Oliver standard auth (MSAL handles CSRF differently via PKCE)
## Sources
- [[daily/2026-04-29.md]] — session 19:06, CSRF protection for stateless JWT refresh endpoint

View file

@ -0,0 +1,102 @@
---
title: "ETag / If-Match Optimistic Locking"
aliases: [etag-locking, optimistic-locking, if-match-patch, 412-precondition]
tags: [fastapi, react, http, concurrency, optimistic-locking, api-design]
sources:
- "daily/2026-04-29.md"
created: 2026-04-29
updated: 2026-04-29
---
# ETag / If-Match Optimistic Locking
Prevents two users from silently overwriting each other's edits without server-side locking. The backend generates an ETag from a content hash; the frontend sends it back on write; stale writes are rejected with 412.
## Key Points
- Backend generates `ETag` from content hash (e.g. `md5(content)` or document version)
- Frontend stores ETag in **React state** (not a ref) — so React Query invalidation triggers a re-fetch on conflict
- PATCH request includes `If-Match: {etag}` header
- Backend checks: if ETag doesn't match current document version → `412 Precondition Failed`
- Frontend `412` handler shows user-visible conflict message and re-fetches latest content
## Details
### Backend (FastAPI)
```python
from hashlib import md5
from fastapi import Header, HTTPException
@router.get("/vtt/{segment_id}")
async def get_vtt(segment_id: str):
doc = await db.vtt_segments.find_one({"_id": segment_id})
content_hash = md5(doc["content"].encode()).hexdigest()
return JSONResponse(
content=doc,
headers={"ETag": f'"{content_hash}"'}
)
@router.patch("/vtt/{segment_id}")
async def update_vtt(
segment_id: str,
payload: VttUpdate,
if_match: str | None = Header(None, alias="If-Match"),
):
doc = await db.vtt_segments.find_one({"_id": segment_id})
current_hash = f'"{md5(doc["content"].encode()).hexdigest()}"'
if if_match and if_match != current_hash:
raise HTTPException(status_code=412, detail="Content modified by another user")
# ... proceed with update
```
### Frontend (React + React Query)
```typescript
// Store ETag in state, not ref — ensures re-render on update
const [etag, setEtag] = useState<string | null>(null);
// On fetch, capture the ETag
const { data } = useQuery(["vtt", segmentId], async () => {
const res = await fetch(`/api/vtt/${segmentId}`);
setEtag(res.headers.get("ETag"));
return res.json();
});
// On save, send If-Match header
const handleSave = async (content: string) => {
const res = await fetch(`/api/vtt/${segmentId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
...(etag ? { "If-Match": etag } : {}),
},
body: JSON.stringify({ content }),
});
if (res.status === 412) {
alert("Someone else edited this segment. Refreshing to show latest version.");
queryClient.invalidateQueries(["vtt", segmentId]);
return;
}
};
```
### Why State, Not Ref
If ETag is stored in a `useRef`, React Query invalidation won't trigger a re-render → the stale ETag persists → the next save attempt uses the old ETag again. Storing in `useState` ensures the component re-renders after invalidation and the fresh ETag is picked up.
## Use Cases
- Collaborative editors (VTT subtitles, document editors)
- Any resource where two users might edit simultaneously (admin panels, CMS)
- Long-lived forms where network latency could cause version skew
## Related Concepts
- [[wiki/concepts/double-submit-cookie-csrf|double-submit-cookie-csrf]] — another HTTP security header pattern for stateless APIs
- [[wiki/tech-patterns/fastapi-python-docker|fastapi-python-docker]] — FastAPI patterns
## Sources
- [[daily/2026-04-29.md]] — session 19:06, VTT editor concurrent edit protection

View file

@ -0,0 +1,97 @@
---
title: "time.sleep() Blocks the Entire asyncio Event Loop"
aliases: [time-sleep-async, asyncio-blocking, sync-sleep-fastapi, asyncio-sleep]
tags: [python, fastapi, asyncio, concurrency, gotcha, performance]
sources:
- "daily/2026-04-29.md"
created: 2026-04-29
updated: 2026-04-29
---
# time.sleep() Blocks the Entire asyncio Event Loop
`time.sleep(N)` called inside any `async def` handler freezes the entire asyncio event loop for the duration — no other requests can be processed. It appears to work in local tests (single request) but silently kills concurrency in production.
## Key Points
- `time.sleep()` is a **synchronous blocking call** — it blocks the OS thread, which blocks the asyncio event loop
- `asyncio.sleep()` is the correct replacement — it yields control back to the event loop while waiting
- For CPU-bound or I/O-bound sync code, use `asyncio.run_in_executor()` to run in a threadpool
- Easy to miss because single-request tests always pass — the bug only appears under concurrent load
## Details
### The Problem
```python
# WRONG — blocks all concurrent requests for 5 seconds
@router.get("/process")
async def process_something():
await do_first_step()
time.sleep(5) # ← entire event loop frozen here
await do_second_step()
return {"status": "done"}
```
While `time.sleep(5)` runs, every other incoming request waits. On a server handling 50 concurrent users, this compounds into multi-second stalls for every user.
### Correct Replacements
**1. Async wait (most common case):**
```python
import asyncio
@router.get("/process")
async def process_something():
await do_first_step()
await asyncio.sleep(5) # ← yields to event loop, other requests continue
await do_second_step()
return {"status": "done"}
```
**2. Sync blocking work in threadpool:**
```python
import asyncio
from concurrent.futures import ThreadPoolExecutor
def heavy_sync_work():
time.sleep(5) # or any blocking I/O
return result
@router.get("/process")
async def process_something():
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, heavy_sync_work)
return {"result": result}
```
**3. FastAPI's `def` (not `async def`) routes — automatic threadpool:**
```python
# FastAPI automatically runs non-async routes in a threadpool
@router.get("/process")
def process_something(): # ← no async keyword
time.sleep(5) # safe here — runs in threadpool
return {"status": "done"}
```
### Code Review Checklist
Search for `time.sleep` as part of any FastAPI code review:
```bash
grep -rn "time\.sleep" app/routers/ app/services/
```
Any hit inside an `async def` function is a bug.
## Related Concepts
- [[wiki/concepts/asyncio-contextvar-task-boundary|asyncio-contextvar-task-boundary]] — other asyncio gotchas in FastAPI
- [[wiki/tech-patterns/fastapi-python-docker|fastapi-python-docker]] — FastAPI best practices
## Sources
- [[daily/2026-04-29.md]] — session 17:51, blocking sleep found in async FastAPI handler

View file

@ -141,3 +141,9 @@
- Articles created: [[wiki/concepts/bash-and-or-short-circuit]], [[wiki/concepts/python-iso-z-suffix]], [[wiki/concepts/gemini-conversation-cost-scaling]]
- Articles updated: [[wiki/concepts/_index]] (concepts 51→54); [[wiki/_master-index]] (concepts 51→54)
- Note: Kling tech-pattern article already up-to-date from prior session; dotfiles Ghostty gotchas deferred (low reuse value vs 3 broadly applicable concepts)
## [2026-04-29T23:00:00+01:00] compile | 2026-04-29.md (pass 2)
- Source: daily/2026-04-29.md
- Articles created: [[wiki/architecture/cloud-run-jobs-celery]], [[wiki/concepts/docker-compose-cpu-limits-env]], [[wiki/concepts/etag-optimistic-locking]], [[wiki/concepts/double-submit-cookie-csrf]], [[wiki/concepts/time-sleep-blocks-asyncio]]
- Articles updated: [[wiki/client-knowledge/ford]] (SSH alias box-cli/not box-cli-01, full directory paths with /home/box-cli/FORD_SCRIPTS/ prefix, service names dev vs prod, GPAS zip naming, git stash deploy pattern, ford-gechub-sftp path)
- Index updates: [[wiki/concepts/_index]] (69→73); [[wiki/architecture/_index]] (10→11); [[wiki/_master-index]] (concepts 69→73, architecture 10→11)