diff --git a/scripts/test_heavy.sh b/scripts/test_heavy.sh new file mode 100755 index 0000000..0b29dbb --- /dev/null +++ b/scripts/test_heavy.sh @@ -0,0 +1,994 @@ +#!/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(array $ctx): array { + 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), + ]; + return ['statusCode' => 200, 'body' => 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' + 200, 'body' => '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 = """ 200, 'body' => 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