#!/usr/bin/env bash # test_heavy.sh — поиск багов: большой код, CPU-нагрузка, большой ответ, # архивы из ZIP, быстрые серии invoke, параллельные вызовы, # медленные функции, давление по памяти. # Все 4 языка: Python, Node.js, Ruby, PHP. # # ВАЖНО: Ruby и PHP invoke ранее не тестировались вообще! # Умеренная нагрузка (кластер небольшой, но нагрузка реальная). # # Запуск: bash scripts/test_heavy.sh # На ВМ: bash ~/terra/fission/scripts/test_heavy.sh set -uo pipefail ROOT_DIR=$(cd "$(dirname "$0")" && pwd) source "${ROOT_DIR}/lib.sh" RUN_ID=$(mk_run_id) SUB="livetest@test.local" TMP=$(mktemp -d) echo "" echo "════════════════════════════════════════════════════════" echo " test_heavy.sh run=${RUN_ID}" echo "════════════════════════════════════════════════════════" declare -a ALL_FNS=() cleanup() { rm -rf "$TMP" 2>/dev/null || true for fn in "${ALL_FNS[@]:-}"; do curl -s -X DELETE "${BASE}/functions/${fn}" \ -H "X-Auth-Token: ${SUB}" >/dev/null 2>&1 || true done } trap cleanup EXIT # ─── helpers ─────────────────────────────────────────────────────────────── # reg FN — регистрирует имя функции для cleanup reg() { ALL_FNS+=("$1"); } # create_json LANG FN CODE_FILE — создаёт функцию из файла с кодом create_json() { local lang="$1" fn="$2" codefile="$3" local code_escaped code_escaped=$(cat "$codefile" | json_escape) reg "$fn" local R R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \ -d "{\"name\":\"${fn}\",\"language\":\"${lang}\",\"code\":${code_escaped}}" 2>/dev/null) check_http "$(echo "$R" | tail -1)" "201" "${lang^^} ${fn} CREATE → 201" } # invoke_once FN EXPECTED LABEL — один invoke, без retry invoke_once() { local fn="$1" expected="$2" label="$3" local resp status raw err resp=$(curl -s --max-time 60 -X POST "${BASE}/functions/${fn}/invoke" \ -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') status=$(json_field "$resp" status) raw=$(json_field "$resp" response_raw) err=$(json_field "$resp" error) echo " fn=$fn status=$status raw=$(printf '%s' "$raw" | head -c 100) err=$(printf '%s' "$err" | head -c 80)" if [ "$status" = "200" ] && printf '%s' "$raw" | grep -qF "$expected"; then pass "$label → 200 + '${expected}'" return 0 else fail "$label → status=${status:-?} (ожидали 200 + '${expected}')" return 1 fi } # ═══════════════════════════════════════════════════════════════════════════ # SECTION 1: БОЛЬШОЙ КОД — ~100 строк на язык, из inline-кода # Ищем: лимиты на размер кода, проблемы загрузки, синтаксические баги # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "────────────────────────────────────────────────────────" echo " S1: БОЛЬШОЙ КОД (Python / Node.js / Ruby / PHP)" echo "────────────────────────────────────────────────────────" FN_BIG_PY="t-big-py-${RUN_ID}" cat > "${TMP}/big_py.py" << 'PYEOF' import math, hashlib, json CONSTS = {f"k{i}": i * 3.14159265 for i in range(20)} def fib(n): a, b = 0, 1 for _ in range(n - 1): a, b = b, a + b return b def sieve(limit): is_p = bytearray([1]) * (limit + 1) is_p[0] = is_p[1] = 0 for i in range(2, int(limit**0.5) + 1): if is_p[i]: for j in range(i*i, limit+1, i): is_p[j] = 0 return sum(is_p) def rot13(s): return s.translate(str.maketrans( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 'nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM')) def caesar(s, shift=7): res = [] for c in s: if 'a' <= c <= 'z': res.append(chr((ord(c) - 97 + shift) % 26 + 97)) elif 'A' <= c <= 'Z': res.append(chr((ord(c) - 65 + shift) % 26 + 65)) else: res.append(c) return ''.join(res) def mat_trace(m): return sum(m[i][i] for i in range(len(m))) def palindrome(s): t = s.lower().replace(' ', '') return t == t[::-1] def flatten(lst): out = [] for item in lst: if isinstance(item, list): out.extend(flatten(item)) else: out.append(item) return out def checksum(data): return hashlib.sha256(data.encode()).hexdigest()[:16] def running_sum(lst): total = 0; out = [] for v in lst: total += v; out.append(total) return out def count_distinct(lst): return len(set(lst)) def group_by_mod(lst, n): g = {i: [] for i in range(n)} for v in lst: g[v % n].append(v) return {str(k): len(v) for k, v in g.items()} def pascal_row(n): row = [1] for i in range(1, n + 1): row.append(row[-1] * (n - i + 1) // i) return row def deduplicate(lst): seen = set(); out = [] for v in lst: if v not in seen: seen.add(v); out.append(v) return out def reverse_words(s): return ' '.join(s.split()[::-1]) def count_vowels(s): return sum(1 for c in s.lower() if c in 'aeiou') def is_prime(n): if n < 2: return False if n == 2: return True if n % 2 == 0: return False for i in range(3, int(math.sqrt(n)) + 1, 2): if n % i == 0: return False return True def main(event, context): mat = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]] data = list(range(1, 51)) nested = [[1,[2,3]],[4,[5,[6,7]]],8] result = { "tag": "big-code-py-ok", "fib_25": fib(25), "primes_50": sieve(50), "rot13": rot13("Hello Fission"), "caesar": caesar("heavy"), "const_sum": round(sum(CONSTS.values()), 2), "mat_trace": mat_trace(mat), "palindrome": palindrome("racecar"), "distinct": count_distinct([1,2,2,3,3,3,4]), "flat_len": len(flatten(nested)), "checksum": checksum("fission-heavy-test"), "rsum_last": running_sum(data)[-1], "pascal_4": pascal_row(4), "dedup_len": len(deduplicate([1,2,2,3,3,3,4,5,5])), "rev_words": reverse_words("big code python test"), "vowels": count_vowels("fission heavy test"), "is_prime_97": is_prime(97), "groups_3": group_by_mod(data[:30], 3), } return json.dumps(result), 200 PYEOF create_json "python" "$FN_BIG_PY" "${TMP}/big_py.py" FN_BIG_NODE="t-big-node-${RUN_ID}" cat > "${TMP}/big_node.js" << 'JSEOF' const crypto = require('crypto'); const CONSTS = {}; for (let i = 0; i < 20; i++) CONSTS[`k${i}`] = i * Math.PI; function fib(n) { let a = 0, b = 1; for (let i = 1; i < n; i++) { let t = a + b; a = b; b = t; } return b; } function sieve(limit) { const isP = new Uint8Array(limit + 1).fill(1); isP[0] = isP[1] = 0; for (let i = 2; i * i <= limit; i++) if (isP[i]) for (let j = i*i; j <= limit; j += i) isP[j] = 0; return isP.reduce((s, v) => s + v, 0); } function rot13(s) { return s.replace(/[a-zA-Z]/g, c => { const b = c <= 'Z' ? 65 : 97; return String.fromCharCode((c.charCodeAt(0) - b + 13) % 26 + b); }); } function caesar(s, shift = 7) { return s.replace(/[a-zA-Z]/g, c => { const b = c <= 'Z' ? 65 : 97; return String.fromCharCode((c.charCodeAt(0) - b + shift) % 26 + b); }); } function matTrace(m) { return m.reduce((s, row, i) => s + row[i], 0); } function palindrome(s) { const t = s.toLowerCase().replace(/ /g, ''); return t === t.split('').reverse().join(''); } function flatten(arr) { return arr.reduce((a, v) => a.concat(Array.isArray(v) ? flatten(v) : v), []); } function checksum(d) { return crypto.createHash('sha256').update(d).digest('hex').slice(0, 16); } function runningSum(arr) { let t = 0; return arr.map(v => (t += v, t)); } function countDistinct(arr) { return new Set(arr).size; } function deduplicate(arr) { const s = new Set(); return arr.filter(v => !s.has(v) && s.add(v)); } function groupByMod(arr, n) { const g = {}; for (let i = 0; i < n; i++) g[i] = 0; arr.forEach(v => g[v % n]++); return g; } function pascalRow(n) { const r = [1]; for (let i = 1; i <= n; i++) r.push((r[i-1] * (n-i+1) / i) | 0); return r; } function reverseWords(s) { return s.split(' ').reverse().join(' '); } function countVowels(s) { return (s.toLowerCase().match(/[aeiou]/g) || []).length; } function isPrime(n) { if (n < 2) return false; for (let i = 2; i * i <= n; i++) if (n % i === 0) return false; return true; } const mat = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]; const nested = [[1,[2,3]],[4,[5,[6,7]]],8]; module.exports = async function(ctx) { const data = Array.from({length: 50}, (_, i) => i + 1); const result = { tag: "big-code-node-ok", fib_25: fib(25), primes_50: sieve(50), rot13: rot13("Hello Fission"), caesar: caesar("heavy"), const_sum: Math.round(Object.values(CONSTS).reduce((a,b) => a+b, 0) * 100) / 100, mat_trace: matTrace(mat), palindrome: palindrome("racecar"), distinct: countDistinct([1,2,2,3,3,3,4]), flat_len: flatten(nested).length, checksum: checksum("fission-heavy-test"), rsum_last: runningSum(data).at(-1), pascal_4: pascalRow(4), dedup_len: deduplicate([1,2,2,3,3,3,4,5,5]).length, rev_words: reverseWords("big code node test"), vowels: countVowels("fission heavy test"), is_prime_97: isPrime(97), groups_3: groupByMod(data.slice(0,30), 3), }; return { status: 200, body: JSON.stringify(result) }; }; JSEOF create_json "nodejs" "$FN_BIG_NODE" "${TMP}/big_node.js" FN_BIG_RUBY="t-big-ruby-${RUN_ID}" cat > "${TMP}/big_ruby.rb" << 'RBEOF' require 'json' require 'digest' CONSTS = (0...20).map { |i| [:"k#{i}", i * Math::PI] }.to_h def fib(n) a, b = 0, 1 (n - 1).times { a, b = b, a + b } b end def sieve(limit) is_p = Array.new(limit + 1, true) is_p[0] = is_p[1] = false (2..Math.sqrt(limit).to_i).each do |i| next unless is_p[i] (i*i..limit).step(i) { |j| is_p[j] = false } end is_p.count(true) end def rot13(s) s.tr('a-zA-Z', 'n-za-mN-ZA-M') end def caesar(s, shift = 7) s.chars.map do |c| if c =~ /[a-z]/ then ((c.ord - 97 + shift) % 26 + 97).chr elsif c =~ /[A-Z]/ then ((c.ord - 65 + shift) % 26 + 65).chr else c end end.join end def mat_trace(m) m.each_with_index.sum { |row, i| row[i] } end def palindrome?(s) t = s.downcase.delete(' ') t == t.reverse end def flatten_nested(lst) lst.each_with_object([]) do |item, out| item.is_a?(Array) ? out.concat(flatten_nested(item)) : out << item end end def checksum(data) Digest::SHA256.hexdigest(data)[0, 16] end def running_sum(arr) total = 0 arr.map { |v| total += v; total } end def count_distinct(arr) arr.uniq.length end def group_by_mod(arr, n) (0...n).each_with_object({}) { |i, h| h[i.to_s] = arr.count { |v| v % n == i } } end def pascal_row(n) row = [1] (1..n).each { |i| row << row[-1] * (n - i + 1) / i } row end def reverse_words(s) s.split.reverse.join(' ') end def count_vowels(s) s.downcase.count('aeiou') end def is_prime(n) return false if n < 2 (2..Math.sqrt(n).to_i).none? { |i| n % i == 0 } end def handler(ctx) mat = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]] data = (1..50).to_a nested = [[1,[2,3]],[4,[5,[6,7]]],8] result = { tag: "big-code-ruby-ok", fib_25: fib(25), primes_50: sieve(50), rot13: rot13("Hello Fission"), caesar: caesar("heavy"), const_sum: CONSTS.values.sum.round(2), mat_trace: mat_trace(mat), palindrome: palindrome?("racecar"), distinct: count_distinct([1,2,2,3,3,3,4]), flat_len: flatten_nested(nested).length, checksum: checksum("fission-heavy-test"), rsum_last: running_sum(data).last, pascal_4: pascal_row(4), rev_words: reverse_words("big code ruby test"), vowels: count_vowels("fission heavy test"), is_prime_97: is_prime(97), groups_3: group_by_mod(data.first(30), 3), } { statusCode: 200, body: result.to_json } end RBEOF create_json "ruby" "$FN_BIG_RUBY" "${TMP}/big_ruby.rb" FN_BIG_PHP="t-big-php-${RUN_ID}" cat > "${TMP}/big_php.php" << 'PHPEOF' = 'a' && $c <= 'z') $r .= chr((ord($c) - 97 + $shift) % 26 + 97); elseif ($c >= 'A' && $c <= 'Z') $r .= chr((ord($c) - 65 + $shift) % 26 + 65); else $r .= $c; } return $r; } function matTrace($m) { $s = 0; foreach ($m as $i => $row) $s += $row[$i]; return $s; } function palindrome($s) { $t = str_replace(' ', '', strtolower($s)); return $t === strrev($t); } function checksumStr($data) { return substr(hash('sha256', $data), 0, 16); } function runningSum($arr) { $t = 0; $out = []; foreach ($arr as $v) { $t += $v; $out[] = $t; } return $out; } function countDistinct($arr) { return count(array_unique($arr)); } function groupByMod($arr, $n) { $g = array_fill(0, $n, 0); foreach ($arr as $v) $g[$v % $n]++; return $g; } function pascalRow($n) { $row = [1]; for ($i = 1; $i <= $n; $i++) $row[] = intdiv($row[$i-1] * ($n-$i+1), $i); return $row; } function reverseWords($s) { return implode(' ', array_reverse(explode(' ', $s))); } function countVowels($s) { return preg_match_all('/[aeiou]/i', $s); } function isPrime($n) { if ($n < 2) return false; for ($i = 2; $i * $i <= $n; $i++) if ($n % $i === 0) return false; return true; } function handler($ctx) { global $CONSTS; $mat = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]; $data = range(1, 50); $constSum = 0; foreach ($CONSTS as $v) $constSum += $v; $rsum = runningSum($data); $result = [ 'tag' => 'big-code-php-ok', 'fib_25' => fib(25), 'primes_50' => sieve(50), 'rot13' => rot13('Hello Fission'), 'caesar' => caesar('heavy'), 'const_sum' => round($constSum, 2), 'mat_trace' => matTrace($mat), 'palindrome' => palindrome('racecar'), 'distinct' => countDistinct([1,2,2,3,3,3,4]), 'checksum' => checksumStr('fission-heavy-test'), 'rsum_last' => end($rsum), 'pascal_4' => pascalRow(4), 'rev_words' => reverseWords('big code php test'), 'vowels' => countVowels('fission heavy test'), 'is_prime_97'=> isPrime(97), 'groups_3' => groupByMod(array_slice($data, 0, 30), 3), ]; $ctx["response"]->getBody()->write(json_encode($result)); } PHPEOF create_json "php" "$FN_BIG_PHP" "${TMP}/big_php.php" echo "" echo ">>> S1: invoke больших функций — ждём cold start всех 4 языков..." for fn_tag in \ "${FN_BIG_PY}:big-code-py-ok:Python" \ "${FN_BIG_NODE}:big-code-node-ok:Node.js" \ "${FN_BIG_RUBY}:big-code-ruby-ok:Ruby" \ "${FN_BIG_PHP}:big-code-php-ok:PHP"; do fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}" ( if wait_invoke "$SUB" "$fn" "$tag" 20 6; then echo "OK" else echo "FAIL" fi ) > "${TMP}/s1-${fn}.out" & done wait for fn_tag in \ "${FN_BIG_PY}:big-code-py-ok:Python" \ "${FN_BIG_NODE}:big-code-node-ok:Node.js" \ "${FN_BIG_RUBY}:big-code-ruby-ok:Ruby" \ "${FN_BIG_PHP}:big-code-php-ok:PHP"; do fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}" result=$(cat "${TMP}/s1-${fn}.out" 2>/dev/null || echo "FAIL") if [ "$result" = "OK" ]; then pass "S1 $label BIG CODE invoke → 200" else fail "S1 $label BIG CODE invoke — не стабилизировался" fi done # ═══════════════════════════════════════════════════════════════════════════ # SECTION 2: CPU BURN — рекурсивный fib, нагрузка на CPU пода # Ищем: OOM kill, timeout, зависание пула, медленный poolmgr # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "────────────────────────────────────────────────────────" echo " S2: CPU BURN — рекурсивный fib (Python/Node.js/Ruby/PHP)" echo "────────────────────────────────────────────────────────" FN_CPU_PY="t-cpu-py-${RUN_ID}" cat > "${TMP}/cpu_py.py" << 'PYEOF' def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) def main(event, context): result = fib(28) return "cpu-burn-py:" + str(result), 200 PYEOF create_json "python" "$FN_CPU_PY" "${TMP}/cpu_py.py" FN_CPU_NODE="t-cpu-node-${RUN_ID}" cat > "${TMP}/cpu_node.js" << 'JSEOF' function fib(n) { if (n < 2) return n; return fib(n-1) + fib(n-2); } module.exports = async function(ctx) { return { status: 200, body: "cpu-burn-node:" + fib(35) }; }; JSEOF create_json "nodejs" "$FN_CPU_NODE" "${TMP}/cpu_node.js" FN_CPU_RUBY="t-cpu-ruby-${RUN_ID}" cat > "${TMP}/cpu_ruby.rb" << 'RBEOF' def fib(n) return n if n < 2 fib(n-1) + fib(n-2) end def handler(ctx) { statusCode: 200, body: "cpu-burn-ruby:#{fib(25)}" } end RBEOF create_json "ruby" "$FN_CPU_RUBY" "${TMP}/cpu_ruby.rb" FN_CPU_PHP="t-cpu-php-${RUN_ID}" cat > "${TMP}/cpu_php.php" << 'PHPEOF' getBody()->write('cpu-burn-php:' . fib(28)); } PHPEOF create_json "php" "$FN_CPU_PHP" "${TMP}/cpu_php.php" echo "" echo ">>> S2: invoke CPU-burn функций..." for fn_tag in \ "${FN_CPU_PY}:cpu-burn-py:Python" \ "${FN_CPU_NODE}:cpu-burn-node:Node.js" \ "${FN_CPU_RUBY}:cpu-burn-ruby:Ruby" \ "${FN_CPU_PHP}:cpu-burn-php:PHP"; do fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}" ( if wait_invoke "$SUB" "$fn" "$tag" 18 6; then echo "OK" else echo "FAIL" fi ) > "${TMP}/s2-${fn}.out" & done wait for fn_tag in \ "${FN_CPU_PY}:cpu-burn-py:Python" \ "${FN_CPU_NODE}:cpu-burn-node:Node.js" \ "${FN_CPU_RUBY}:cpu-burn-ruby:Ruby" \ "${FN_CPU_PHP}:cpu-burn-php:PHP"; do fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}" result=$(cat "${TMP}/s2-${fn}.out" 2>/dev/null || echo "FAIL") if [ "$result" = "OK" ]; then pass "S2 $label CPU BURN invoke → 200" else fail "S2 $label CPU BURN — не стабилизировался" fi done # ═══════════════════════════════════════════════════════════════════════════ # SECTION 3: БОЛЬШОЙ ОТВЕТ — 50KB body # Ищем: truncation, proxy memory issues, timeout на больших ответах # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "────────────────────────────────────────────────────────" echo " S3: БОЛЬШОЙ ОТВЕТ — 50KB (Python + Node.js)" echo "────────────────────────────────────────────────────────" FN_RESP_PY="t-resp-py-${RUN_ID}" cat > "${TMP}/resp_py.py" << 'PYEOF' def main(event, context): chunk = "FISSION-HEAVY-DATA-" * 100 # 1900 chars data = (chunk * 27)[:50000] # ровно 50000 chars return "bigresponse-py-ok:" + data[:20] + "..." + str(len(data)), 200 PYEOF create_json "python" "$FN_RESP_PY" "${TMP}/resp_py.py" FN_RESP_NODE="t-resp-node-${RUN_ID}" cat > "${TMP}/resp_node.js" << 'JSEOF' module.exports = async function(ctx) { const chunk = "FISSION-HEAVY-DATA-".repeat(100); const data = (chunk.repeat(27)).slice(0, 50000); return { status: 200, body: "bigresponse-node-ok:" + data.slice(0,20) + "..." + data.length }; }; JSEOF create_json "nodejs" "$FN_RESP_NODE" "${TMP}/resp_node.js" echo "" echo ">>> S3: invoke и проверка размера ответа..." for fn_tag in \ "${FN_RESP_PY}:bigresponse-py-ok:Python" \ "${FN_RESP_NODE}:bigresponse-node-ok:Node.js"; do fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}" if wait_invoke "$SUB" "$fn" "$tag" 15 6; then # Проверяем реальный размер ответа RESP=$(curl -s --max-time 60 -X POST "${BASE}/functions/${fn}/invoke" \ -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') RAW=$(json_field "$RESP" response_raw) RAW_LEN=${#RAW} echo " response_raw length = $RAW_LEN chars" if [ "$RAW_LEN" -gt 40000 ]; then pass "S3 $label BIG RESPONSE size=$RAW_LEN (>40000)" else fail "S3 $label BIG RESPONSE слишком мал: size=$RAW_LEN" fi else fail "S3 $label BIG RESPONSE — invoke не стабилизировался" fi done # ═══════════════════════════════════════════════════════════════════════════ # SECTION 4: АРХИВЫ ИЗ ZIP — создаём архивы на лету, все 4 языка # Ищем: баги пути upload vs inline, entrypoint defaults, storagesvc PV # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "────────────────────────────────────────────────────────" echo " S4: АРХИВ ИЗ ZIP (Python / Node.js / Ruby / PHP)" echo "────────────────────────────────────────────────────────" # --- Создаём ZIP-архивы на лету через python3 --- # Python archive: main.py (entrypoint по умолчанию: main.main) python3 - > "${TMP}/arc-py.zip" << 'EOF' import zipfile, io, sys buf = io.BytesIO() code = """import json def main(event, context): return json.dumps({"tag": "archive-py-ok", "fib25": 75025}), 200 """ with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: zf.writestr('main.py', code) sys.stdout.buffer.write(buf.getvalue()) EOF # Node.js archive: main.js (entrypoint: main → файл main.js) python3 - > "${TMP}/arc-node.zip" << 'EOF' import zipfile, io, sys buf = io.BytesIO() code = """module.exports = async function(ctx) { return { status: 200, body: JSON.stringify({ tag: "archive-node-ok", fib25: 75025 }) }; }; """ pkg = '{"name":"heavy-node","version":"1.0.0"}' with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: zf.writestr('main.js', code) zf.writestr('package.json', pkg) sys.stdout.buffer.write(buf.getvalue()) EOF # Ruby archive: handler.rb (entrypoint: handler) python3 - > "${TMP}/arc-ruby.zip" << 'EOF' import zipfile, io, sys buf = io.BytesIO() code = """require 'json' def handler(ctx) { statusCode: 200, body: JSON.generate({ tag: "archive-ruby-ok", fib25: 75025 }) } end """ with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: zf.writestr('handler.rb', code) sys.stdout.buffer.write(buf.getvalue()) EOF # PHP archive: main.php (entrypoint: main.php::handler) python3 - > "${TMP}/arc-php.zip" << 'EOF' import zipfile, io, sys buf = io.BytesIO() code = """getBody()->write(json_encode(['tag' => 'archive-php-ok', 'fib25' => 75025])); } """ with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: zf.writestr('main.php', code) sys.stdout.buffer.write(buf.getvalue()) EOF # Загружаем архивы через multipart POST /functions declare -A ARC_LANGS=( ["py"]="python" ["node"]="nodejs" ["ruby"]="ruby" ["php"]="php" ) declare -A ARC_TAGS=( ["py"]="archive-py-ok" ["node"]="archive-node-ok" ["ruby"]="archive-ruby-ok" ["php"]="archive-php-ok" ) for lang_short in py node ruby php; do fn="t-arc-${lang_short}-${RUN_ID}" reg "$fn" lang="${ARC_LANGS[$lang_short]}" arcfile="${TMP}/arc-${lang_short}.zip" R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \ -H "X-Auth-Token: ${SUB}" \ -F "name=${fn}" \ -F "language=${lang}" \ -F "archive=@${arcfile};type=application/zip" 2>/dev/null) CODE=$(echo "$R" | tail -1) BODY=$(echo "$R" | head -n -1) check_http "$CODE" "201" "S4 ${lang_short^^} ARCHIVE upload → 201" if [ "$CODE" != "201" ]; then echo " upload error: $(printf '%s' "$BODY" | head -c 200)" fi done echo "" echo ">>> S4: invoke архивных функций — ждём cold start..." for lang_short in py node ruby php; do fn="t-arc-${lang_short}-${RUN_ID}" tag="${ARC_TAGS[$lang_short]}" ( if wait_invoke "$SUB" "$fn" "$tag" 20 6; then echo "OK" else echo "FAIL" fi ) > "${TMP}/s4-${fn}.out" & done wait for lang_short in py node ruby php; do fn="t-arc-${lang_short}-${RUN_ID}" tag="${ARC_TAGS[$lang_short]}" result=$(cat "${TMP}/s4-${fn}.out" 2>/dev/null || echo "FAIL") if [ "$result" = "OK" ]; then pass "S4 ${lang_short^^} ARCHIVE invoke → 200" else fail "S4 ${lang_short^^} ARCHIVE invoke — не стабилизировался" fi done # ═══════════════════════════════════════════════════════════════════════════ # SECTION 5: БЫСТРЫЕ СЕРИИ — 12 последовательных invoke одной функции # Ищем: poolmgr reuse bugs, утечки памяти, деградацию latency # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "────────────────────────────────────────────────────────" echo " S5: БЫСТРЫЕ СЕРИИ — 12x invoke подряд (Python)" echo "────────────────────────────────────────────────────────" FN_RAPID="t-rapid-py-${RUN_ID}" cat > "${TMP}/rapid_py.py" << 'PYEOF' import time _count = 0 def main(event, context): global _count _count += 1 return "rapid-ok:{}".format(_count), 200 PYEOF create_json "python" "$FN_RAPID" "${TMP}/rapid_py.py" echo ">>> Ждём cold start..." if wait_invoke "$SUB" "$FN_RAPID" "rapid-ok" 15 5; then echo ">>> S5: Запускаем 12 последовательных invoke..." RAPID_OK=0 RAPID_FAIL=0 LATENCIES=() for i in $(seq 1 12); do T_START=$(date +%s%3N) RESP=$(curl -s --max-time 20 -X POST "${BASE}/functions/${FN_RAPID}/invoke" \ -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') T_END=$(date +%s%3N) LAT=$(( T_END - T_START )) STATUS=$(json_field "$RESP" status) RAW=$(json_field "$RESP" response_raw) echo " invoke ${i}/12: status=$STATUS raw=$(printf '%s' "$RAW" | head -c 40) latency=${LAT}ms" if [ "$STATUS" = "200" ]; then RAPID_OK=$((RAPID_OK+1)) LATENCIES+=("$LAT") else RAPID_FAIL=$((RAPID_FAIL+1)) fi done [ "$RAPID_FAIL" -eq 0 ] && pass "S5 RAPID FIRE: 12/12 invoke → 200" \ || fail "S5 RAPID FIRE: ${RAPID_FAIL}/12 упали" # Статистика latency if [ "${#LATENCIES[@]}" -gt 0 ]; then LAT_MIN="${LATENCIES[0]}"; LAT_MAX="${LATENCIES[0]}" for lat in "${LATENCIES[@]}"; do [ "$lat" -lt "$LAT_MIN" ] && LAT_MIN="$lat" [ "$lat" -gt "$LAT_MAX" ] && LAT_MAX="$lat" done echo " latency min=${LAT_MIN}ms max=${LAT_MAX}ms" fi else fail "S5 RAPID FIRE — cold start не прошёл, тест пропущен" fi # ═══════════════════════════════════════════════════════════════════════════ # SECTION 6: ПАРАЛЛЕЛЬНЫЕ ВЫЗОВЫ — 5 параллельных invoke на каждый язык # Ищем: race conditions, конфликты при pod scheduling, deadlocks # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "────────────────────────────────────────────────────────" echo " S6: ПАРАЛЛЕЛЬНЫЕ ВЫЗОВЫ — 5 одновременных invoke × 4 языка" echo "────────────────────────────────────────────────────────" PARALLEL_N=5 # Используем уже тёплые функции из S2 (CPU burn — уже invoke'd) declare -A PAR_FNS=( ["Python"]="$FN_CPU_PY" ["Node.js"]="$FN_CPU_NODE" ["Ruby"]="$FN_CPU_RUBY" ["PHP"]="$FN_CPU_PHP" ) declare -A PAR_TAGS=( ["Python"]="cpu-burn-py" ["Node.js"]="cpu-burn-node" ["Ruby"]="cpu-burn-ruby" ["PHP"]="cpu-burn-php" ) for lang in "Python" "Node.js" "Ruby" "PHP"; do fn="${PAR_FNS[$lang]}" tag="${PAR_TAGS[$lang]}" echo "" echo ">>> S6 $lang: $PARALLEL_N параллельных invoke..." for i in $(seq 1 "$PARALLEL_N"); do ( RESP=$(curl -s --max-time 60 -X POST "${BASE}/functions/${fn}/invoke" \ -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') STATUS=$(json_field "$RESP" status) RAW=$(json_field "$RESP" response_raw) if [ "$STATUS" = "200" ] && printf '%s' "$RAW" | grep -qF "$tag"; then echo "OK" > "${TMP}/par-${lang}-${i}.out" else echo "FAIL:status=${STATUS}" > "${TMP}/par-${lang}-${i}.out" fi ) & done wait PAR_OK=0; PAR_FAIL=0 for i in $(seq 1 "$PARALLEL_N"); do result=$(cat "${TMP}/par-${lang}-${i}.out" 2>/dev/null || echo "FAIL:no-file") if [ "$result" = "OK" ]; then PAR_OK=$((PAR_OK+1)) else PAR_FAIL=$((PAR_FAIL+1)) echo " invoke $i: $result" fi done [ "$PAR_FAIL" -eq 0 ] \ && pass "S6 $lang PARALLEL: ${PARALLEL_N}/${PARALLEL_N} invoke → 200" \ || fail "S6 $lang PARALLEL: ${PAR_FAIL}/${PARALLEL_N} упали" done # ═══════════════════════════════════════════════════════════════════════════ # SECTION 7: МЕДЛЕННЫЕ ФУНКЦИИ — sleep 6s, Python + Node.js # Ищем: router timeout, poolmgr idle eviction, 504 под нагрузкой # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "────────────────────────────────────────────────────────" echo " S7: МЕДЛЕННЫЕ ФУНКЦИИ — sleep(6s) × Python + Node.js" echo "────────────────────────────────────────────────────────" FN_SLOW_PY="t-slow-py-${RUN_ID}" cat > "${TMP}/slow_py.py" << 'PYEOF' import time def main(event, context): time.sleep(6) return "slow-py-ok", 200 PYEOF create_json "python" "$FN_SLOW_PY" "${TMP}/slow_py.py" FN_SLOW_NODE="t-slow-node-${RUN_ID}" cat > "${TMP}/slow_node.js" << 'JSEOF' module.exports = async function(ctx) { await new Promise(r => setTimeout(r, 6000)); return { status: 200, body: "slow-node-ok" }; }; JSEOF create_json "nodejs" "$FN_SLOW_NODE" "${TMP}/slow_node.js" echo "" echo ">>> S7: invoke (ждём cold start + 6s sleep)..." for fn_tag in \ "${FN_SLOW_PY}:slow-py-ok:Python" \ "${FN_SLOW_NODE}:slow-node-ok:Node.js"; do fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}" T_START=$(date +%s%3N) if wait_invoke "$SUB" "$fn" "$tag" 15 8; then T_END=$(date +%s%3N) LAT=$(( T_END - T_START )) pass "S7 $label SLOW(6s) → 200 за ~${LAT}ms total" # Повторный invoke (warm pod): должно занять ≥6s T2=$(date +%s%3N) RESP=$(curl -s --max-time 30 -X POST "${BASE}/functions/${fn}/invoke" \ -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') T2E=$(date +%s%3N) LAT2=$(( T2E - T2 )) STATUS=$(json_field "$RESP" status) RAW=$(json_field "$RESP" response_raw) echo " warm invoke: status=$STATUS latency=${LAT2}ms raw=$(printf '%s' "$RAW" | head -c 40)" if [ "$STATUS" = "200" ] && [ "$LAT2" -ge 5000 ]; then pass "S7 $label SLOW warm invoke → 200 latency=${LAT2}ms (≥5s)" elif [ "$STATUS" = "200" ]; then fail "S7 $label SLOW warm слишком быстро: ${LAT2}ms — sleep не сработал?" else fail "S7 $label SLOW warm invoke → status=$STATUS" fi else fail "S7 $label SLOW — cold start не прошёл" fi done # ═══════════════════════════════════════════════════════════════════════════ # SECTION 8: ДАВЛЕНИЕ ПО ПАМЯТИ — аллокация 20MB, Python + Node.js # Ищем: OOM kill на поде, зависание, некорректный ответ # ═══════════════════════════════════════════════════════════════════════════ echo "" echo "────────────────────────────────────────────────────────" echo " S8: ПАМЯТЬ — аллокация 20MB (Python + Node.js)" echo "────────────────────────────────────────────────────────" FN_MEM_PY="t-mem-py-${RUN_ID}" cat > "${TMP}/mem_py.py" << 'PYEOF' def main(event, context): # Аллоцируем 20MB и суммируем (чтобы не оптимизировалось) data = bytearray(20 * 1024 * 1024) for i in range(0, len(data), 4096): data[i] = i % 256 checksum = sum(data[i] for i in range(0, len(data), 4096)) % 65536 return "mem-py-ok:{}:{}".format(len(data), checksum), 200 PYEOF create_json "python" "$FN_MEM_PY" "${TMP}/mem_py.py" FN_MEM_NODE="t-mem-node-${RUN_ID}" cat > "${TMP}/mem_node.js" << 'JSEOF' module.exports = async function(ctx) { const size = 20 * 1024 * 1024; const buf = Buffer.alloc(size); for (let i = 0; i < size; i += 4096) buf[i] = i % 256; let checksum = 0; for (let i = 0; i < size; i += 4096) checksum = (checksum + buf[i]) % 65536; return { status: 200, body: `mem-node-ok:${size}:${checksum}` }; }; JSEOF create_json "nodejs" "$FN_MEM_NODE" "${TMP}/mem_node.js" echo "" echo ">>> S8: invoke (20MB аллокация)..." for fn_tag in \ "${FN_MEM_PY}:mem-py-ok:Python" \ "${FN_MEM_NODE}:mem-node-ok:Node.js"; do fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}" if wait_invoke "$SUB" "$fn" "$tag" 18 6; then pass "S8 $label MEMORY(20MB) → 200" # Проверяем что данные в ответе правильные RESP=$(curl -s --max-time 30 -X POST "${BASE}/functions/${fn}/invoke" \ -H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}') RAW=$(json_field "$RESP" response_raw) if printf '%s' "$RAW" | grep -qE "$tag:[0-9]+:[0-9]+"; then pass "S8 $label MEMORY ответ содержит checksum: $(printf '%s' "$RAW" | head -c 60)" else fail "S8 $label MEMORY ответ некорректен: $(printf '%s' "$RAW" | head -c 60)" fi else fail "S8 $label MEMORY(20MB) — не вернулась (OOM kill?)" fi done # ═══════════════════════════════════════════════════════════════════════════ print_summary