diff --git a/examples/POSTGRES/chaos_marathon.tf b/examples/POSTGRES/chaos_marathon.tf deleted file mode 100644 index 88ee5a7..0000000 --- a/examples/POSTGRES/chaos_marathon.tf +++ /dev/null @@ -1,257 +0,0 @@ -// 2026-03-21 — chaos_marathon.tf: 15 новых сервисов для часового хаос-марафона. -// Два рантайма: python3.11 (9), nodejs20 (2). -// Все зависят от sless_job.postgres_table_init_job. - -# ── Python: работа с таблицей ───────────────────────────────────────────────── - -# Считает строки по prefix — тест concurrent reads + COUNT агрегации. -resource "sless_service" "pg_counter" { - name = "pg-counter" - runtime = "python3.11" - entrypoint = "pg_counter.count" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-counter" - depends_on = [sless_job.postgres_table_init_job] -} - -# DELETE дублей по title — идемпотентный, повторный вызов безопасен. -resource "sless_service" "pg_dedup" { - name = "pg-dedup" - runtime = "python3.11" - entrypoint = "pg_dedup.dedup" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-dedup" - depends_on = [sless_job.postgres_table_init_job] -} - -# Поиск по title с ILIKE + пагинация — тест спецсимволов и SQL injection safety. -resource "sless_service" "pg_search" { - name = "pg-search" - runtime = "python3.11" - entrypoint = "pg_search.search" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-search" - depends_on = [sless_job.postgres_table_init_job] -} - -# Bulk INSERT через execute_values — до 500 строк за раз. -resource "sless_service" "pg_bulk_insert" { - name = "pg-bulk-insert" - runtime = "python3.11" - entrypoint = "pg_bulk_insert.bulk_insert" - memory_mb = 256 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-bulk-insert" - depends_on = [sless_job.postgres_table_init_job] -} - -# DELETE строк старше N минут — идемпотентный. -resource "sless_service" "pg_delete_old" { - name = "pg-delete-old" - runtime = "python3.11" - entrypoint = "pg_delete_old.delete_old" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-delete-old" - depends_on = [sless_job.postgres_table_init_job] -} - -# INSERT ON CONFLICT DO UPDATE — повторный вызов с тем же title безопасен. -resource "sless_service" "pg_upsert" { - name = "pg-upsert" - runtime = "python3.11" - entrypoint = "pg_upsert.upsert" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/pg-upsert" - depends_on = [sless_job.postgres_table_init_job] -} - -# ── Python: chaos ───────────────────────────────────────────────────────────── - -# Echo: принимает любой ввод и отражает обратно — проверка на мусорный input. -resource "sless_service" "chaos_echo" { - name = "chaos-echo" - runtime = "python3.11" - entrypoint = "chaos_echo.echo" - memory_mb = 128 - timeout_sec = 10 - - source_dir = "${path.module}/code/chaos-echo" - depends_on = [sless_job.postgres_table_init_job] -} - -# Валидация плохих параметров — тупой юзер не может уронить сервис. -resource "sless_service" "chaos_badparams" { - name = "chaos-badparams" - runtime = "python3.11" - entrypoint = "chaos_badparams.validate" - memory_mb = 128 - timeout_sec = 10 - - source_dir = "${path.module}/code/chaos-badparams" - depends_on = [sless_job.postgres_table_init_job] -} - -# Медленный pg_sleep — тест timeout enforcement. -resource "sless_service" "chaos_slowquery" { - name = "chaos-slowquery" - runtime = "python3.11" - entrypoint = "chaos_slowquery.slowquery" - memory_mb = 128 - timeout_sec = 12 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/chaos-slowquery" - depends_on = [sless_job.postgres_table_init_job] -} - -# Большой JSON response — тест памяти и серилизации. -resource "sless_service" "chaos_bigpayload" { - name = "chaos-bigpayload" - runtime = "python3.11" - entrypoint = "chaos_bigpayload.bigpayload" - memory_mb = 256 - timeout_sec = 15 - - source_dir = "${path.module}/code/chaos-bigpayload" - depends_on = [sless_job.postgres_table_init_job] -} - -# ── Node.js ─────────────────────────────────────────────────────────────────── - -# Bulk INSERT через параметризованный multi-value query. -resource "sless_service" "js_pg_batch" { - name = "js-pg-batch" - runtime = "nodejs20" - entrypoint = "js_pg_batch.run" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/js-pg-batch" - depends_on = [sless_job.postgres_table_init_job] -} - -# Идемпотентный INSERT — повторный вызов с тем же key = existing, не дубль. -resource "sless_service" "js_idempotent" { - name = "js-idempotent" - runtime = "nodejs20" - entrypoint = "js_idempotent.run" - memory_mb = 128 - timeout_sec = 15 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/js-idempotent" - depends_on = [sless_job.postgres_table_init_job] -} - -# ── Python: retry ───────────────────────────────────────────────────────────── - -# Запись с retry при transient PG error — тест устойчивости к сбоям. -resource "sless_service" "py_retry_writer" { - name = "py-retry-writer" - runtime = "python3.11" - entrypoint = "py_retry_writer.retry_write" - memory_mb = 128 - timeout_sec = 30 - - env_vars = { - PGHOST = local.pg_host - PGPORT = "5432" - PGDATABASE = local.pg_database - PGUSER = local.pg_username - PGPASSWORD = local.pg_password - PGSSLMODE = "require" - } - - source_dir = "${path.module}/code/py-retry-writer" - depends_on = [sless_job.postgres_table_init_job] -} diff --git a/examples/POSTGRES/code/chaos-badparams/chaos_badparams.py b/examples/POSTGRES/code/chaos-badparams/chaos_badparams.py deleted file mode 100644 index cdfe032..0000000 --- a/examples/POSTGRES/code/chaos-badparams/chaos_badparams.py +++ /dev/null @@ -1,40 +0,0 @@ -# 2026-03-21 — chaos-badparams: проверяет что функция не падает на мусорных входных данных. -# Принимает type=missing|wrong_type|huge|negative|zero и возвращает safe-ответ. -# Тестирует: устойчивость к "тупому юзеру" — никакого 500 на плохих входных данных. -import json - -_MAX_N = 10_000 - -def validate(event): - errors = [] - results = {} - - # n: должно быть int от 1 до MAX_N - raw_n = event.get("n") - try: - n = int(raw_n) - if n <= 0: - errors.append(f"n must be > 0, got {n}") - n = 1 - elif n > _MAX_N: - errors.append(f"n capped from {n} to {_MAX_N}") - n = _MAX_N - except (TypeError, ValueError): - errors.append(f"n is not a valid int: {repr(raw_n)}, using default 1") - n = 1 - results["n"] = n - - # name: обрезаем до 100 символов - raw_name = event.get("name", "") - if not isinstance(raw_name, str): - raw_name = str(raw_name) - errors.append("name was not a string, converted") - name = raw_name[:100] - results["name"] = name - - # flag: любое "truthy" значение - raw_flag = event.get("flag", False) - flag = raw_flag in (True, "true", "1", 1, "yes") - results["flag"] = flag - - return {"ok": len(errors) == 0, "errors": errors, "results": results} diff --git a/examples/POSTGRES/code/chaos-bigpayload/chaos_bigpayload.py b/examples/POSTGRES/code/chaos-bigpayload/chaos_bigpayload.py deleted file mode 100644 index 523e4d2..0000000 --- a/examples/POSTGRES/code/chaos-bigpayload/chaos_bigpayload.py +++ /dev/null @@ -1,27 +0,0 @@ -# 2026-03-21 — chaos-bigpayload: генерирует/принимает большой JSON. -# Тестирует: большие ответы (64KB+), память рантайма. -import json, time - -def bigpayload(event): - size_kb = min(int(event.get("size_kb", 16)), 256) # cap 256KB - word = str(event.get("word", "x"))[:32] - - # Генерируем список строк нужного размера - chunk = word * 32 # ~32+ байт на запись - items = [] - total = 0 - target = size_kb * 1024 - i = 0 - while total < target: - entry = f"{chunk}-{i}" - items.append(entry) - total += len(entry) + 3 # 3 байта JSON overhead - i += 1 - - return { - "items_count": len(items), - "size_kb_approx": round(total / 1024, 1), - "first": items[0] if items else "", - "last": items[-1] if items else "", - "ts": int(time.time()), - } diff --git a/examples/POSTGRES/code/chaos-echo/chaos_echo.py b/examples/POSTGRES/code/chaos-echo/chaos_echo.py deleted file mode 100644 index 276023e..0000000 --- a/examples/POSTGRES/code/chaos-echo/chaos_echo.py +++ /dev/null @@ -1,19 +0,0 @@ -# 2026-03-21 — chaos-echo: отражает входные данные обратно. -# Тестирует: большие payload, unicode, null, вложенные структуры, спецсимволы. -# "Тупой юзер" шлёт всё что угодно — функция должна вернуть это обратно без падения. -import json - -def echo(event): - # Пытаемся сериализовать обратно — выловит непериализуемые типы - try: - size = len(json.dumps(event)) - except Exception: - size = -1 - - keys = list(event.keys()) if isinstance(event, dict) else [] - return { - "echo": event, - "keys": keys, - "size_bytes": size, - "type": type(event).__name__, - } diff --git a/examples/POSTGRES/code/chaos-slowquery/chaos_slowquery.py b/examples/POSTGRES/code/chaos-slowquery/chaos_slowquery.py deleted file mode 100644 index 6b9fb42..0000000 --- a/examples/POSTGRES/code/chaos-slowquery/chaos_slowquery.py +++ /dev/null @@ -1,19 +0,0 @@ -# 2026-03-21 — chaos-slowquery: намеренно медленный запрос через pg_sleep. -# Тестирует: timeout enforcement — платформа должна прервать запрос если > timeout_sec. -# sleep_sec cap = 8 (меньше timeout_sec=10 сервиса → успех; >10 → таймаут платформы). -import os, psycopg2 - -def slowquery(event): - sleep_sec = min(float(event.get("sleep_sec", 2.0)), 8.0) - conn = psycopg2.connect( - host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)), - dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"), - ) - try: - with conn.cursor() as cur: - cur.execute("SELECT pg_sleep(%s), now()::text", (sleep_sec,)) - result = cur.fetchone() - return {"slept_sec": sleep_sec, "pg_now": result[1]} - finally: - conn.close() diff --git a/examples/POSTGRES/code/chaos-slowquery/requirements.txt b/examples/POSTGRES/code/chaos-slowquery/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/chaos-slowquery/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/funcs-list/funcs_list.py b/examples/POSTGRES/code/funcs-list/funcs_list.py deleted file mode 100644 index 68157d2..0000000 --- a/examples/POSTGRES/code/funcs-list/funcs_list.py +++ /dev/null @@ -1,94 +0,0 @@ -# 2026-03-18 (обновлено: plain text вывод; фильтрация SLESS_EXCLUDE) -# funcs_list.py — HTTP-функция: список пользовательских функций, человекочитаемый plain text. -# Вызывает внутренний REST API оператора (ClusterIP, без TLS). -# Возвращает str → python runtime отдаёт text/plain напрямую без json.dumps. -# -# Env vars: -# SLESS_API_URL — URL оператора (http://sless-operator.sless.svc.cluster.local:9090) -# SLESS_NAMESPACE — namespace пользователя (sless-{hex16}) -# SLESS_TOKEN — JWT токен для /v1/ API -# SLESS_EXTERNAL_URL — публичный базовый URL (https://sless.kube5s.ru) -# SLESS_EXCLUDE — comma-separated имена функций, которые не показывать - -import os -import requests - -SEP = "─" * 52 - - -def _comment(fn, http_trigs, cron_trigs): - phase = fn.get("phase", "?") - runtime = fn.get("runtime", "?") - if http_trigs: - active = "активна" if http_trigs[0].get("active") else "неактивна" - return f"HTTP endpoint ({runtime}) — {phase}, {active}" - elif cron_trigs: - schedule = cron_trigs[0].get("schedule", "?") - active = "активна" if cron_trigs[0].get("active") else "неактивна" - return f"Cron '{schedule}' ({runtime}) — {phase}, {active}" - else: - return f"Job/runner без триггера ({runtime}) — {phase}" - - -def list_all(event): - api_url = os.environ["SLESS_API_URL"].rstrip("/") - namespace = os.environ["SLESS_NAMESPACE"] - token = os.environ["SLESS_TOKEN"] - ext_url = os.environ.get("SLESS_EXTERNAL_URL", "").rstrip("/") - exclude = {n.strip() for n in os.environ.get("SLESS_EXCLUDE", "").split(",") if n.strip()} - - headers = {"Authorization": f"Bearer {token}"} - fns = requests.get(f"{api_url}/v1/namespaces/{namespace}/functions", headers=headers, timeout=10) - trs = requests.get(f"{api_url}/v1/namespaces/{namespace}/triggers", headers=headers, timeout=10) - fns.raise_for_status() - trs.raise_for_status() - - trig_idx = {} - for tr in trs.json(): - fn_name = tr.get("function") or tr.get("functionRef") - if fn_name: - trig_idx.setdefault(fn_name, []).append(tr) - - items = [] - for fn in fns.json(): - name = fn["name"] - if name in exclude: - continue - http_t = [t for t in trig_idx.get(name, []) if t.get("type") == "http"] - cron_t = [t for t in trig_idx.get(name, []) if t.get("type") == "cron"] - is_active = any(t.get("enabled", True) and t.get("active", False) for t in trig_idx.get(name, [])) - items.append((fn, http_t, cron_t, is_active)) - - # Сортировка: активные вверх, затем по имени - items.sort(key=lambda x: (not x[3], x[0]["name"])) - - lines = [] - for fn, http_t, cron_t, is_active in items: - name = fn["name"] - lines.append(SEP) - lines.append(f" {_comment(fn, http_t, cron_t)}") - lines.append(f" name: {name}") - lines.append(f" runtime: {fn.get('runtime', '?')}") - lines.append(f" phase: {fn.get('phase', '?')}") - lines.append(f" active: {'да' if is_active else 'нет'}") - - if http_t: - url = f"{ext_url}/fn/{namespace}/{name}" if ext_url else http_t[0].get("url", "") - lines.append(f" url: {url}") - if cron_t: - lines.append(f" cron: {cron_t[0].get('schedule', '?')}") - if fn.get("created_at"): - lines.append(f" created: {fn['created_at']}") - if fn.get("last_built_at"): - lines.append(f" built: {fn['last_built_at']}") - if fn.get("message"): - lines.append(f" message: {fn['message']}") - - lines.append(SEP) - lines.append(f" namespace: {namespace} | total: {len(items)}") - lines.append(SEP) - - # Возвращаем str — python runtime отдаст text/plain напрямую - return "\n".join(lines) + "\n" - - diff --git a/examples/POSTGRES/code/funcs-list/requirements.txt b/examples/POSTGRES/code/funcs-list/requirements.txt deleted file mode 100644 index 2c24336..0000000 --- a/examples/POSTGRES/code/funcs-list/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.31.0 diff --git a/examples/POSTGRES/code/js-pg-batch/js_pg_batch.js b/examples/POSTGRES/code/js-pg-batch/js_pg_batch.js deleted file mode 100644 index c95eb45..0000000 --- a/examples/POSTGRES/code/js-pg-batch/js_pg_batch.js +++ /dev/null @@ -1,43 +0,0 @@ -// 2026-03-21 — js-pg-batch: вставляет N строк через parameterized bulk query. -// Тестирует: async/await PG с пакетной вставкой, Node.js под нагрузкой. -const { Client } = require('pg'); - -async function run(event) { - const n = Math.min(parseInt(event.n ?? 20, 10) || 20, 200); - const prefix = String(event.prefix ?? 'js-batch').slice(0, 40); - - const client = new Client({ - host: process.env.PGHOST, - port: parseInt(process.env.PGPORT ?? '5432'), - database: process.env.PGDATABASE, - user: process.env.PGUSER, - password: process.env.PGPASSWORD, - ssl: { rejectUnauthorized: false }, - }); - await client.connect(); - - try { - const ts = Date.now(); - // Строим multi-value INSERT: INSERT INTO ... VALUES ($1), ($2), ... - const placeholders = []; - const values = []; - for (let i = 0; i < n; i++) { - placeholders.push(`($${i + 1})`); - values.push(`${prefix}-${ts}-${i}`); - } - const sql = `INSERT INTO terraform_demo_table (title) VALUES ${placeholders.join(',')} RETURNING id`; - const t0 = Date.now(); - const res = await client.query(sql, values); - const elapsed = (Date.now() - t0) / 1000; - - return { - inserted: res.rowCount, - first_id: res.rows[0]?.id ?? null, - elapsed_sec: elapsed, - }; - } finally { - await client.end(); - } -} - -module.exports = { run }; diff --git a/examples/POSTGRES/code/js-pg-batch/package.json b/examples/POSTGRES/code/js-pg-batch/package.json deleted file mode 100644 index f484622..0000000 --- a/examples/POSTGRES/code/js-pg-batch/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "js-pg-batch", - "version": "1.0.0", - "dependencies": { - "pg": "^8.11.3" - } -} diff --git a/examples/POSTGRES/code/pg-bulk-insert/pg_bulk_insert.py b/examples/POSTGRES/code/pg-bulk-insert/pg_bulk_insert.py deleted file mode 100644 index 555ffbe..0000000 --- a/examples/POSTGRES/code/pg-bulk-insert/pg_bulk_insert.py +++ /dev/null @@ -1,38 +0,0 @@ -# 2026-03-21 — pg-bulk-insert: bulk INSERT через execute_values. -# Тестирует: большие батчи (до 500 строк), производительность, память. -import os, time, psycopg2, psycopg2.extras - -def bulk_insert(event): - try: - n = max(0, min(int(event.get("n", 50)), 500)) # cap 500, min 0 - except (TypeError, ValueError): - n = 50 - prefix = str(event.get("prefix", "bulk"))[:50] - ts = int(time.time() * 1000) - - # n=0 — граничный случай: вернуть сразу без обращения к PG. - if n == 0: - return {"inserted": 0, "first_id": None, "elapsed_sec": 0.0} - - rows = [(f"{prefix}-{ts}-{i}",) for i in range(n)] - - conn = psycopg2.connect( - host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)), - dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"), - ) - try: - t0 = time.time() - with conn.cursor() as cur: - psycopg2.extras.execute_values( - cur, - "INSERT INTO terraform_demo_table (title) VALUES %s RETURNING id", - rows, - page_size=100, - ) - ids = [r[0] for r in cur.fetchall()] - conn.commit() - elapsed = round(time.time() - t0, 3) - return {"inserted": len(ids), "first_id": ids[0] if ids else None, "elapsed_sec": elapsed} - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-bulk-insert/requirements.txt b/examples/POSTGRES/code/pg-bulk-insert/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-bulk-insert/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/pg-dedup/pg_dedup.py b/examples/POSTGRES/code/pg-dedup/pg_dedup.py deleted file mode 100644 index 18cba85..0000000 --- a/examples/POSTGRES/code/pg-dedup/pg_dedup.py +++ /dev/null @@ -1,38 +0,0 @@ -# 2026-03-21 — pg-dedup: удаляет дубликаты по title, оставляет первый (min id). -# Тестирует: DELETE с subquery, CTE, idempotency (повторный вызов безопасен). -import os, psycopg2 - -def dedup(event): - dry_run = str(event.get("dry_run", "false")).lower() in ("true", "1", "yes") - conn = psycopg2.connect( - host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)), - dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"), - ) - try: - with conn.cursor() as cur: - # Считаем сколько дублей есть - cur.execute(""" - SELECT COUNT(*) FROM terraform_demo_table t1 - WHERE EXISTS ( - SELECT 1 FROM terraform_demo_table t2 - WHERE t2.title = t1.title AND t2.id < t1.id - ) - """) - dupes_count = cur.fetchone()[0] - - if not dry_run and dupes_count > 0: - cur.execute(""" - DELETE FROM terraform_demo_table - WHERE id NOT IN ( - SELECT MIN(id) FROM terraform_demo_table GROUP BY title - ) - """) - deleted = cur.rowcount - conn.commit() - else: - deleted = 0 - - return {"duplicates_found": dupes_count, "deleted": deleted, "dry_run": dry_run} - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-dedup/requirements.txt b/examples/POSTGRES/code/pg-dedup/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-dedup/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/pg-delete-old/pg_delete_old.py b/examples/POSTGRES/code/pg-delete-old/pg_delete_old.py deleted file mode 100644 index d9655b3..0000000 --- a/examples/POSTGRES/code/pg-delete-old/pg_delete_old.py +++ /dev/null @@ -1,33 +0,0 @@ -# 2026-03-21 — pg-delete-old: удаляет строки старше N минут (default 60). -# Тестирует: DELETE с RETURNING, идемпотентность (повторный вызов = 0 удалений если нет старых). -import os, psycopg2, psycopg2.extras - -def delete_old(event): - older_than_min = max(int(event.get("older_than_min", 60)), 1) - prefix_filter = event.get("prefix", "") - - conn = psycopg2.connect( - host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)), - dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"), - ) - try: - with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: - if prefix_filter: - cur.execute( - "DELETE FROM terraform_demo_table " - "WHERE created_at < now() - interval '1 minute' * %s " - "AND title LIKE %s RETURNING id, title", - (older_than_min, f"{prefix_filter}%"), - ) - else: - cur.execute( - "DELETE FROM terraform_demo_table " - "WHERE created_at < now() - interval '1 minute' * %s RETURNING id, title", - (older_than_min,), - ) - deleted = [dict(r) for r in cur.fetchall()] - conn.commit() - return {"deleted": len(deleted), "older_than_min": older_than_min, "sample": deleted[:5]} - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-delete-old/requirements.txt b/examples/POSTGRES/code/pg-delete-old/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-delete-old/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/pg-search/pg_search.py b/examples/POSTGRES/code/pg-search/pg_search.py deleted file mode 100644 index fa62669..0000000 --- a/examples/POSTGRES/code/pg-search/pg_search.py +++ /dev/null @@ -1,36 +0,0 @@ -# 2026-03-21 — pg-search: полнотекстовый поиск по title через ILIKE + LIMIT/OFFSET. -# Тестирует: пагинацию, спецсимволы в input (XSS, SQL injection attempt → безопасно через параметры). -import os, psycopg2, psycopg2.extras - -def search(event): - # «query» — основной параметр (user-friendly), «q» — алиас для совместимости. - query = str(event.get("query") or event.get("q") or "")[:200] - # int() может упасть если юзер прислал строку — защищаем try/except. - try: - limit = max(1, min(int(event.get("limit", 20)), 100)) - except (TypeError, ValueError): - limit = 20 - try: - offset = max(0, int(event.get("offset", 0))) - except (TypeError, ValueError): - offset = 0 - - conn = psycopg2.connect( - host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)), - dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"), - ) - try: - with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: - pattern = f"%{query}%" if query else "%" - cur.execute( - "SELECT id, title, created_at::text FROM terraform_demo_table " - "WHERE title ILIKE %s ORDER BY id DESC LIMIT %s OFFSET %s", - (pattern, limit, offset), - ) - rows = [dict(r) for r in cur.fetchall()] - cur.execute("SELECT COUNT(*) FROM terraform_demo_table WHERE title ILIKE %s", (pattern,)) - total = cur.fetchone()["count"] - return {"rows": rows, "count": len(rows), "total": total, "q": query, "limit": limit, "offset": offset} - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-search/requirements.txt b/examples/POSTGRES/code/pg-search/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-search/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/pg-upsert/pg_upsert.py b/examples/POSTGRES/code/pg-upsert/pg_upsert.py deleted file mode 100644 index 7159cfc..0000000 --- a/examples/POSTGRES/code/pg-upsert/pg_upsert.py +++ /dev/null @@ -1,36 +0,0 @@ -# 2026-03-21 — pg-upsert: INSERT ... ON CONFLICT (title) DO UPDATE. -# Тестирует: идемпотентность вставки — один и тот же title можно вызывать 100 раз подряд. -# Требует уникального индекса на title — создаётся при первом вызове (CREATE UNIQUE INDEX IF NOT EXISTS). -import os, psycopg2 - -def upsert(event): - title = str(event.get("title", "upsert-default"))[:255] - payload = str(event.get("payload", ""))[:500] - - conn = psycopg2.connect( - host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)), - dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"), - ) - try: - with conn.cursor() as cur: - # Создаём уникальный индекс если нет — для поддержки ON CONFLICT - cur.execute( - "CREATE UNIQUE INDEX IF NOT EXISTS terraform_demo_table_title_uniq " - "ON terraform_demo_table (title)" - ) - cur.execute( - "INSERT INTO terraform_demo_table (title) VALUES (%s) " - "ON CONFLICT (title) DO UPDATE SET created_at = now() " - "RETURNING id, title, created_at::text, xmax", - (title,), - ) - row = cur.fetchone() - was_insert = row[3] == 0 # xmax=0 означает INSERT, иначе UPDATE - conn.commit() - return { - "id": row[0], "title": row[1], "created_at": row[2], - "action": "inserted" if was_insert else "updated", - } - finally: - conn.close() diff --git a/examples/POSTGRES/code/pg-upsert/requirements.txt b/examples/POSTGRES/code/pg-upsert/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/pg-upsert/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/py-retry-writer/py_retry_writer.py b/examples/POSTGRES/code/py-retry-writer/py_retry_writer.py deleted file mode 100644 index fbc5e56..0000000 --- a/examples/POSTGRES/code/py-retry-writer/py_retry_writer.py +++ /dev/null @@ -1,54 +0,0 @@ -# 2026-03-21 — py-retry-writer: пишет N строк с retry при PG ошибке. -# Тестирует: устойчивость к transient PG errors (simulate_error=true), retry logic, -# корректный rollback при частичном сбое. -import os, time, psycopg2, random - -_MAX_RETRIES = 3 - -def retry_write(event): - n = min(int(event.get("n", 5)), 100) - prefix = str(event.get("prefix", "retry"))[:40] - # simulate_error: с вероятностью 30% кидает OperationalError на 2-й попытке - simulate = str(event.get("simulate_error", "false")).lower() in ("true", "1") - - attempt = 0 - last_err = None - - while attempt < _MAX_RETRIES: - attempt += 1 - try: - conn = psycopg2.connect( - host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)), - dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"), - ) - inserted = [] - try: - with conn.cursor() as cur: - for i in range(n): - # Симуляция: на первой попытке падаем с вероятностью 50% - if simulate and attempt == 1 and i == n // 2: - raise psycopg2.OperationalError("simulated transient error") - title = f"{prefix}-{int(time.time()*1000)}-{i}-a{attempt}" - cur.execute( - "INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id", - (title,), - ) - inserted.append(cur.fetchone()[0]) - conn.commit() - return { - "ok": True, "inserted": len(inserted), - "attempts": attempt, "first_id": inserted[0] if inserted else None, - } - except Exception as e: - conn.rollback() - raise - finally: - conn.close() - except psycopg2.OperationalError as e: - last_err = str(e) - if attempt < _MAX_RETRIES: - time.sleep(0.3 * attempt) # exponential backoff - continue - - return {"ok": False, "attempts": attempt, "last_error": last_err} diff --git a/examples/POSTGRES/code/py-retry-writer/requirements.txt b/examples/POSTGRES/code/py-retry-writer/requirements.txt deleted file mode 100644 index 37ec460..0000000 --- a/examples/POSTGRES/code/py-retry-writer/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary diff --git a/examples/POSTGRES/code/sql-runner/requirements.txt b/examples/POSTGRES/code/sql-runner/requirements.txt deleted file mode 100644 index 56ae88a..0000000 --- a/examples/POSTGRES/code/sql-runner/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -# 2026-03-17 00:00 -# requirements.txt — зависимости для функции запуска SQL. -psycopg2-binary==2.9.9 diff --git a/examples/POSTGRES/code/sql-runner/sql_runner.py b/examples/POSTGRES/code/sql-runner/sql_runner.py deleted file mode 100644 index cca9e03..0000000 --- a/examples/POSTGRES/code/sql-runner/sql_runner.py +++ /dev/null @@ -1,39 +0,0 @@ -# 2026-03-17 00:00 -# sql_runner.py — функция для выполнения SQL-операторов из входного события. -import os -import psycopg2 - - -def run_sql(event): - # Выполняет список SQL-операторов в одной транзакции для атомарной инициализации схемы. - # Параметры подключения передаются раздельно, чтобы избежать ошибок парсинга DSN при спецсимволах. - pg_host = os.environ["PGHOST"] - pg_port = os.environ.get("PGPORT", "5432") - pg_database = os.environ["PGDATABASE"] - pg_user = os.environ["PGUSER"] - pg_password = os.environ["PGPASSWORD"] - pg_sslmode = os.environ.get("PGSSLMODE", "require") - statements = event.get("statements", []) - - if not statements: - return {"error": "no statements provided"} - - connection = psycopg2.connect( - host=pg_host, - port=pg_port, - dbname=pg_database, - user=pg_user, - password=pg_password, - sslmode=pg_sslmode, - ) - try: - cursor = connection.cursor() - for statement in statements: - cursor.execute(statement) - connection.commit() - return {"ok": True, "executed": len(statements)} - except Exception as error: - connection.rollback() - return {"error": str(error)} - finally: - connection.close() diff --git a/examples/POSTGRES/code/stress-bigloop/requirements.txt b/examples/POSTGRES/code/stress-bigloop/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/POSTGRES/code/stress-bigloop/stress_bigloop.py b/examples/POSTGRES/code/stress-bigloop/stress_bigloop.py deleted file mode 100644 index c9ce935..0000000 --- a/examples/POSTGRES/code/stress-bigloop/stress_bigloop.py +++ /dev/null @@ -1,20 +0,0 @@ -# 2026-03-19 -# stress_bigloop.py — CPU-интенсивная функция: считает сумму квадратов N чисел. -# Проверяет поведение под нагрузкой (большая и средняя итерация). - -import time - -_VERSION = "v1" - - -def run(event): - n = int(event.get("n", 500_000)) - start = time.monotonic() - total = sum(i * i for i in range(n)) - elapsed = round(time.monotonic() - start, 4) - return { - "version": _VERSION, - "n": n, - "sum_of_squares": total, - "elapsed_sec": elapsed, - } diff --git a/examples/POSTGRES/code/stress-divzero/requirements.txt b/examples/POSTGRES/code/stress-divzero/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/POSTGRES/code/stress-divzero/stress_divzero.py b/examples/POSTGRES/code/stress-divzero/stress_divzero.py deleted file mode 100644 index 86746e5..0000000 --- a/examples/POSTGRES/code/stress-divzero/stress_divzero.py +++ /dev/null @@ -1,13 +0,0 @@ -# 2026-03-19 -# stress_divzero.py — намеренно делит на ноль (ZeroDivisionError). -# Проверяет: платформа перехватывает панику, возвращает HTTP 500, не роняет под. - -_VERSION = "v1" - - -def run(event): - numerator = int(event.get("n", 42)) - denominator = int(event.get("d", 0)) # по умолчанию 0 — намеренный краш - # ZeroDivisionError: проверяем что платформа обрабатывает исключения - result = numerator / denominator - return {"version": _VERSION, "result": result} diff --git a/examples/POSTGRES/code/stress-js-async/package.json b/examples/POSTGRES/code/stress-js-async/package.json deleted file mode 100644 index 554d7bc..0000000 --- a/examples/POSTGRES/code/stress-js-async/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "stress-js-async", - "version": "1.0.0", - "dependencies": { - "pg": "^8.11.0" - } -} diff --git a/examples/POSTGRES/code/stress-js-async/stress_js_async.js b/examples/POSTGRES/code/stress-js-async/stress_js_async.js deleted file mode 100644 index 022c2b3..0000000 --- a/examples/POSTGRES/code/stress-js-async/stress_js_async.js +++ /dev/null @@ -1,37 +0,0 @@ -// 2026-03-19 -// stress_js_async.js — делает 3 параллельных запроса к PG через Promise.all. -// Проверяет nodejs20 runtime под умеренной нагрузкой и async/await. -// -// Entrypoint: stress_js_async.run - -'use strict'; - -const { Client } = require('pg'); - -exports.run = async (event) => { - const client = new Client({ - host: process.env.PGHOST, - port: parseInt(process.env.PGPORT || '5432'), - database: process.env.PGDATABASE, - user: process.env.PGUSER, - password: process.env.PGPASSWORD, - ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false, - }); - await client.connect(); - try { - const [ver, cnt, max] = await Promise.all([ - client.query('SELECT version() AS v'), - client.query('SELECT COUNT(*) AS cnt FROM terraform_demo_table'), - client.query('SELECT MAX(id) AS max_id FROM terraform_demo_table'), - ]); - return { - runtime: 'nodejs20', - version: 'v1', - pg_version: ver.rows[0].v.split(' ').slice(0, 2).join(' '), - total_rows: parseInt(cnt.rows[0].cnt, 10), - max_id: max.rows[0].max_id, - }; - } finally { - await client.end(); - } -}; diff --git a/examples/POSTGRES/code/stress-js-badenv/package.json b/examples/POSTGRES/code/stress-js-badenv/package.json deleted file mode 100644 index 1b3892c..0000000 --- a/examples/POSTGRES/code/stress-js-badenv/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "stress-js-badenv", - "version": "1.0.0", - "dependencies": {} -} diff --git a/examples/POSTGRES/code/stress-js-badenv/stress_js_badenv.js b/examples/POSTGRES/code/stress-js-badenv/stress_js_badenv.js deleted file mode 100644 index 7169fd8..0000000 --- a/examples/POSTGRES/code/stress-js-badenv/stress_js_badenv.js +++ /dev/null @@ -1,17 +0,0 @@ -// 2026-03-19 -// stress_js_badenv.js — читает несуществующую переменную env и падает. -// Проверяет: платформа перехватывает TypeError/undefined, возвращает 500. -// -// Entrypoint: stress_js_badenv.run - -'use strict'; - -exports.run = async (event) => { - const crash = event.crash !== false; // по умолчанию crash=true - if (crash) { - // Читаем несуществующий env, пытаемся вызвать .toUpperCase() на undefined - const val = process.env.THIS_VAR_DOES_NOT_EXIST_AT_ALL; - return { shout: val.toUpperCase() }; // TypeError: Cannot read properties of undefined - } - return { runtime: 'nodejs20', version: 'v1', crashed: false }; -}; diff --git a/examples/POSTGRES/code/stress-slow/requirements.txt b/examples/POSTGRES/code/stress-slow/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/examples/POSTGRES/code/stress-slow/stress_slow.py b/examples/POSTGRES/code/stress-slow/stress_slow.py deleted file mode 100644 index ba54397..0000000 --- a/examples/POSTGRES/code/stress-slow/stress_slow.py +++ /dev/null @@ -1,18 +0,0 @@ -# 2026-03-19 -# stress_slow.py — долгая функция: спит N секунд (по умолчанию 8). -# Проверяет что timeout-механизм и параллельные запросы не блокируют друг друга. - -import time -import os - -_VERSION = "v1" - - -def run(event): - secs = int(event.get("sleep", 8)) - time.sleep(secs) - return { - "version": _VERSION, - "slept_sec": secs, - "pid": os.getpid(), - } diff --git a/examples/POSTGRES/code/stress-writer/requirements.txt b/examples/POSTGRES/code/stress-writer/requirements.txt deleted file mode 100644 index 58ab769..0000000 --- a/examples/POSTGRES/code/stress-writer/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary==2.9.9 diff --git a/examples/POSTGRES/code/stress-writer/stress_writer.py b/examples/POSTGRES/code/stress-writer/stress_writer.py deleted file mode 100644 index 7eda871..0000000 --- a/examples/POSTGRES/code/stress-writer/stress_writer.py +++ /dev/null @@ -1,39 +0,0 @@ -# 2026-03-19 -# stress_writer.py — пишет N строк в terraform_demo_table (по умолчанию 5). -# Проверяет параллельные INSERT'ы и устойчивость соединения с PG при нагрузке. - -import os -import psycopg2 -import time - -_VERSION = "v1" - - -def run(event): - n = int(event.get("rows", 5)) - prefix = event.get("prefix", "stress") - - conn = psycopg2.connect( - host=os.environ["PGHOST"], - port=int(os.environ.get("PGPORT", "5432")), - dbname=os.environ["PGDATABASE"], - user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], - sslmode=os.environ.get("PGSSLMODE", "require"), - ) - inserted = [] - try: - with conn.cursor() as cur: - for i in range(n): - title = f"{prefix}-{int(time.time()*1000)}-{i}" - cur.execute( - "INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id", - (title,), - ) - row = cur.fetchone() - inserted.append({"id": row[0], "title": title}) - conn.commit() - finally: - conn.close() - - return {"version": _VERSION, "inserted": inserted, "count": len(inserted)} diff --git a/examples/POSTGRES/code/table-rw/requirements.txt b/examples/POSTGRES/code/table-rw/requirements.txt deleted file mode 100644 index 58ab769..0000000 --- a/examples/POSTGRES/code/table-rw/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psycopg2-binary==2.9.9 diff --git a/examples/POSTGRES/code/table-rw/table_rw.py b/examples/POSTGRES/code/table-rw/table_rw.py deleted file mode 100644 index d142e34..0000000 --- a/examples/POSTGRES/code/table-rw/table_rw.py +++ /dev/null @@ -1,133 +0,0 @@ -# 2026-03-19 — добавлен version и hostname в ответ list_rows для тестирования обновления кода -# table_rw.py — чтение и запись строк в terraform_demo_table. -# Два entrypoint в одном файле: list_rows (JSON API) и add_row (HTML-страница + POST-обработчик). -# ENV: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE - -import os -import json -import socket -import psycopg2 -import psycopg2.extras - -_CODE_VERSION = "v2-with-hostname" - - -def _connect(): - return psycopg2.connect( - host=os.environ["PGHOST"], - port=os.environ.get("PGPORT", "5432"), - dbname=os.environ["PGDATABASE"], - user=os.environ["PGUSER"], - password=os.environ["PGPASSWORD"], - sslmode=os.environ.get("PGSSLMODE", "require"), - ) - - -def list_rows(event): - # Возвращает все строки terraform_demo_table, отсортированные по убыванию created_at. - conn = _connect() - try: - cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - cur.execute( - "SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC" - ) - rows = [dict(r) for r in cur.fetchall()] - return {"rows": rows, "count": len(rows), "version": _CODE_VERSION, "host": socket.gethostname()} - finally: - conn.close() - - -def _render_page(rows, message=""): - # HTML-страница с формой ввода и таблицей строк. - # message — статус последней операции (успех / ошибка). - rows_html = "".join( - f"
{message}
' if message else "" - return f""" - - - -| # | title | created_at |
|---|