update docs
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
def
|
||||
44
Makefile
Normal file
44
Makefile
Normal file
@@ -0,0 +1,44 @@
|
||||
.PHONY: up down seed invoke install logs console clean graphs docs
|
||||
|
||||
PY ?= .venv/bin/python
|
||||
PIP ?= .venv/bin/pip
|
||||
DOT_SRC := $(wildcard docs/graphs/*.dot)
|
||||
SVG_OUT := $(DOT_SRC:.dot=.svg)
|
||||
|
||||
install:
|
||||
python3 -m venv .venv
|
||||
$(PIP) install -U pip
|
||||
$(PIP) install -r requirements.txt
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
@echo "MinIO API: http://localhost:9000"
|
||||
@echo "MinIO console: http://localhost:9001 (minioadmin / minioadmin)"
|
||||
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
clean:
|
||||
docker compose down -v
|
||||
|
||||
logs:
|
||||
docker compose logs -f minio
|
||||
|
||||
seed:
|
||||
@if [ -z "$$SOURCE_DIR" ]; then echo "set SOURCE_DIR=<path-to-pdfs>"; exit 2; fi
|
||||
$(PY) seed.py "$$SOURCE_DIR"
|
||||
|
||||
invoke:
|
||||
$(PY) invoke.py
|
||||
|
||||
console:
|
||||
xdg-open http://localhost:9001 >/dev/null 2>&1 || true
|
||||
|
||||
docs/graphs/%.svg: docs/graphs/%.dot
|
||||
dot -Tsvg $< -o $@
|
||||
|
||||
graphs: $(SVG_OUT)
|
||||
@echo "rendered $(words $(SVG_OUT)) svg(s) from $(words $(DOT_SRC)) dot file(s)"
|
||||
|
||||
docs: $(SVG_OUT)
|
||||
xdg-open docs/index.html >/dev/null 2>&1 || echo "open docs/index.html in your browser"
|
||||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: ethics-minio
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
minio-data:
|
||||
55
docs/graphs/cold_warm_timeline.dot
Normal file
55
docs/graphs/cold_warm_timeline.dot
Normal file
@@ -0,0 +1,55 @@
|
||||
digraph cold_warm_timeline {
|
||||
rankdir=LR
|
||||
bgcolor="#0a0e17"
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0" shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
||||
|
||||
label="Cold vs warm — what gets billed, what gets measured"
|
||||
labelloc=t
|
||||
fontsize=16
|
||||
fontcolor="#0066ff"
|
||||
|
||||
subgraph cluster_cold {
|
||||
label="INVOCATION 1 — cold (Init Duration shows in CloudWatch)"
|
||||
style=dashed
|
||||
color="#ff3d00"
|
||||
fontcolor="#ff3d00"
|
||||
|
||||
c_dl [label="Download code\n~50–200 ms\n(NOT billed)" fillcolor="#121829" fontcolor="#8892a8"]
|
||||
c_init [label="Init phase\n~200–800 ms typical\n(boto3/aioboto3 imports,\nclient build)\n(billed at full mem)" fillcolor="#1a1a3a" fontcolor="#ffc107"]
|
||||
c_handler [label="Handler\n~5–500 ms\n(billed)" fillcolor="#0d1a33"]
|
||||
c_freeze [label="freeze" fillcolor="#121829" fontcolor="#8892a8"]
|
||||
}
|
||||
|
||||
subgraph cluster_warm1 {
|
||||
label="INVOCATION 2 — warm (no Init Duration logged)"
|
||||
style=dashed
|
||||
color="#00c853"
|
||||
fontcolor="#00c853"
|
||||
|
||||
w_thaw [label="thaw\nmicroseconds\n(NOT billed)" fillcolor="#121829" fontcolor="#8892a8"]
|
||||
w_handler [label="Handler\n~5–500 ms\n(billed)" fillcolor="#0d1a33" fontcolor="#00c853"]
|
||||
w_freeze [label="freeze" fillcolor="#121829" fontcolor="#8892a8"]
|
||||
}
|
||||
|
||||
subgraph cluster_warm2 {
|
||||
label="INVOCATION 3 — warm"
|
||||
style=dashed
|
||||
color="#00c853"
|
||||
fontcolor="#00c853"
|
||||
|
||||
w2_thaw [label="thaw" fillcolor="#121829" fontcolor="#8892a8"]
|
||||
w2_handler [label="Handler\n(billed)" fillcolor="#0d1a33" fontcolor="#00c853"]
|
||||
}
|
||||
|
||||
notes [label="Init Duration is ONLY in cold-start logs.\nDuration is the handler portion only.\nBilled Duration rounds Duration up to 1 ms.\nWith Provisioned Concurrency, init runs ahead of time —\nyou pay for it in PC pricing, not per invocation." fillcolor="#0a0e17" color="#1e2a4a" shape=note fontcolor="#b4bccf" fontsize=10]
|
||||
|
||||
c_dl -> c_init -> c_handler -> c_freeze
|
||||
c_freeze -> w_thaw [label="next event\n(< idle window)" color="#00c853"]
|
||||
w_thaw -> w_handler -> w_freeze
|
||||
w_freeze -> w2_thaw [color="#00c853"]
|
||||
w2_thaw -> w2_handler
|
||||
|
||||
{rank=sink; notes}
|
||||
}
|
||||
158
docs/graphs/cold_warm_timeline.svg
Normal file
158
docs/graphs/cold_warm_timeline.svg
Normal file
@@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: cold_warm_timeline Pages: 1 -->
|
||||
<svg width="1511pt" height="223pt"
|
||||
viewBox="0.00 0.00 1511.00 223.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 219.38)">
|
||||
<title>cold_warm_timeline</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-219.38 1506.75,-219.38 1506.75,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="751.38" y="-196.18" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Cold vs warm — what gets billed, what gets measured</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_cold</title>
|
||||
<polygon fill="#0a0e17" stroke="#ff3d00" stroke-dasharray="5,2" points="8,-8 8,-127 519.75,-127 519.75,-8 8,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="263.88" y="-107.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#ff3d00">INVOCATION 1 — cold (Init Duration shows in CloudWatch)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_warm1</title>
|
||||
<polygon fill="#0a0e17" stroke="#00c853" stroke-dasharray="5,2" points="611,-22 611,-114 1017,-114 1017,-22 611,-22"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="814" y="-94.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#00c853">INVOCATION 2 — warm (no Init Duration logged)</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_warm2</title>
|
||||
<polygon fill="#0a0e17" stroke="#00c853" stroke-dasharray="5,2" points="1038,-28 1038,-108 1494.75,-108 1494.75,-28 1038,-28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1266.38" y="-88.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#00c853">INVOCATION 3 — warm</text>
|
||||
</g>
|
||||
<!-- c_dl -->
|
||||
<g id="node1" class="node">
|
||||
<title>c_dl</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="116,-78.25 16,-78.25 16,-29.75 116,-29.75 116,-78.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="66" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">Download code</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="66" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">~50–200 ms</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="66" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">(NOT billed)</text>
|
||||
</g>
|
||||
<!-- c_init -->
|
||||
<g id="node2" class="node">
|
||||
<title>c_init</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="306.25,-91.75 153,-91.75 153,-16.25 306.25,-16.25 306.25,-91.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-77.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">Init phase</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">~200–800 ms typical</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(boto3/aioboto3 imports,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">client build)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-23.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(billed at full mem)</text>
|
||||
</g>
|
||||
<!-- c_dl->c_init -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>c_dl->c_init</title>
|
||||
<path fill="none" stroke="#4a5568" d="M116.37,-54C124.38,-54 132.87,-54 141.45,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="141.25,-57.5 151.25,-54 141.25,-50.5 141.25,-57.5"/>
|
||||
</g>
|
||||
<!-- c_handler -->
|
||||
<g id="node3" class="node">
|
||||
<title>c_handler</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="420.75,-78.25 343.25,-78.25 343.25,-29.75 420.75,-29.75 420.75,-78.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="382" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">Handler</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="382" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">~5–500 ms</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="382" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(billed)</text>
|
||||
</g>
|
||||
<!-- c_init->c_handler -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>c_init->c_handler</title>
|
||||
<path fill="none" stroke="#4a5568" d="M306.69,-54C315.11,-54 323.52,-54 331.48,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="331.28,-57.5 341.28,-54 331.28,-50.5 331.28,-57.5"/>
|
||||
</g>
|
||||
<!-- c_freeze -->
|
||||
<g id="node4" class="node">
|
||||
<title>c_freeze</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="511.75,-72 457.75,-72 457.75,-36 511.75,-36 511.75,-72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="484.75" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">freeze</text>
|
||||
</g>
|
||||
<!-- c_handler->c_freeze -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>c_handler->c_freeze</title>
|
||||
<path fill="none" stroke="#4a5568" d="M421.09,-54C429.25,-54 437.87,-54 446.02,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="446,-57.5 456,-54 446,-50.5 446,-57.5"/>
|
||||
</g>
|
||||
<!-- w_thaw -->
|
||||
<g id="node5" class="node">
|
||||
<title>w_thaw</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="756.62,-78.25 664.88,-78.25 664.88,-29.75 756.62,-29.75 756.62,-78.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="710.75" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">thaw</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="710.75" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">microseconds</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="710.75" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">(NOT billed)</text>
|
||||
</g>
|
||||
<!-- c_freeze->w_thaw -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>c_freeze->w_thaw</title>
|
||||
<path fill="none" stroke="#00c853" d="M511.88,-54C546.28,-54 607.68,-54 652.99,-54"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="652.92,-57.5 662.92,-54 652.92,-50.5 652.92,-57.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="565.38" y="-67.95" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">next event</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="565.38" y="-56.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">(< idle window)</text>
|
||||
</g>
|
||||
<!-- w_handler -->
|
||||
<g id="node6" class="node">
|
||||
<title>w_handler</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="871.12,-78.25 793.62,-78.25 793.62,-29.75 871.12,-29.75 871.12,-78.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="832.38" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">Handler</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="832.38" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">~5–500 ms</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="832.38" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">(billed)</text>
|
||||
</g>
|
||||
<!-- w_thaw->w_handler -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>w_thaw->w_handler</title>
|
||||
<path fill="none" stroke="#4a5568" d="M756.98,-54C765.16,-54 773.73,-54 782.03,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="781.99,-57.5 791.99,-54 781.99,-50.5 781.99,-57.5"/>
|
||||
</g>
|
||||
<!-- w_freeze -->
|
||||
<g id="node7" class="node">
|
||||
<title>w_freeze</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="962.12,-72 908.12,-72 908.12,-36 962.12,-36 962.12,-72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="935.12" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">freeze</text>
|
||||
</g>
|
||||
<!-- w_handler->w_freeze -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>w_handler->w_freeze</title>
|
||||
<path fill="none" stroke="#4a5568" d="M871.46,-54C879.63,-54 888.25,-54 896.39,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="896.37,-57.5 906.37,-54 896.37,-50.5 896.37,-57.5"/>
|
||||
</g>
|
||||
<!-- w2_thaw -->
|
||||
<g id="node8" class="node">
|
||||
<title>w2_thaw</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="1100,-72 1046,-72 1046,-36 1100,-36 1100,-72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1073" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">thaw</text>
|
||||
</g>
|
||||
<!-- w_freeze->w2_thaw -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>w_freeze->w2_thaw</title>
|
||||
<path fill="none" stroke="#00c853" d="M962.32,-54C982.81,-54 1011.57,-54 1034.5,-54"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="1034.37,-57.5 1044.37,-54 1034.37,-50.5 1034.37,-57.5"/>
|
||||
</g>
|
||||
<!-- w2_handler -->
|
||||
<g id="node9" class="node">
|
||||
<title>w2_handler</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="1486.75,-72 1428,-72 1428,-36 1486.75,-36 1486.75,-72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1457.38" y="-57.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">Handler</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1457.38" y="-43.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">(billed)</text>
|
||||
</g>
|
||||
<!-- w2_thaw->w2_handler -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>w2_thaw->w2_handler</title>
|
||||
<path fill="none" stroke="#4a5568" d="M1100.28,-54C1166,-54 1337.82,-54 1416.27,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1416.07,-57.5 1426.07,-54 1416.07,-50.5 1416.07,-57.5"/>
|
||||
</g>
|
||||
<!-- notes -->
|
||||
<g id="node10" class="node">
|
||||
<title>notes</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" points="1404,-187.88 1118,-187.88 1118,-116.12 1410,-116.12 1410,-181.88 1404,-187.88"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="1404,-187.88 1404,-181.88"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="1410,-181.88 1404,-181.88"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-174.38" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">Init Duration is ONLY in cold-start logs.</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-161.62" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">Duration is the handler portion only.</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-148.88" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">Billed Duration rounds Duration up to 1 ms.</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-136.12" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">With Provisioned Concurrency, init runs ahead of time —</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-123.38" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">you pay for it in PC pricing, not per invocation.</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
58
docs/graphs/lifecycle.dot
Normal file
58
docs/graphs/lifecycle.dot
Normal file
@@ -0,0 +1,58 @@
|
||||
digraph lifecycle {
|
||||
rankdir=TB
|
||||
bgcolor="#0a0e17"
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0"]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
||||
|
||||
label="Lambda execution environment lifecycle"
|
||||
labelloc=t
|
||||
fontsize=16
|
||||
fontcolor="#0066ff"
|
||||
|
||||
subgraph cluster_cold {
|
||||
label="Cold start (first invocation on a fresh execution environment)"
|
||||
style=dashed
|
||||
color="#ff3d00"
|
||||
fontcolor="#ff3d00"
|
||||
|
||||
download [label="1. Download code\nzip / container layers" fillcolor="#243056" shape=box]
|
||||
bootstrap [label="2. Start runtime\nbootstrap (python3.x)" fillcolor="#243056" shape=box]
|
||||
init [label="3. Init phase\nrun module-level code\nimport boto3 / aioboto3\nbuild clients\n(billed; capped at 10 s)" fillcolor="#1a1a3a" shape=box fontcolor="#ffc107"]
|
||||
}
|
||||
|
||||
subgraph cluster_invoke {
|
||||
label="Invocation"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
handler [label="handler(event, context)\nyour code runs\n(billed)" fillcolor="#0d1a33" shape=box]
|
||||
respond [label="return / raise" fillcolor="#121829" shape=box]
|
||||
}
|
||||
|
||||
subgraph cluster_warm {
|
||||
label="Warm reuse (subsequent invocations on the same environment)"
|
||||
style=dashed
|
||||
color="#00c853"
|
||||
fontcolor="#00c853"
|
||||
|
||||
thaw [label="thaw\n(microseconds)" fillcolor="#1a3a1a" shape=box]
|
||||
reuse [label="globals retained:\nclients, /tmp,\nin-memory caches" fillcolor="#1a3a1a" shape=note fontcolor="#00c853"]
|
||||
}
|
||||
|
||||
freeze [label="freeze\nprocess paused\n(after handler returns)" fillcolor="#121829" shape=box]
|
||||
shutdown [label="shutdown\nidle ~5–15 min →\nenv torn down\n/tmp gone" fillcolor="#121829" shape=box fontcolor="#ff3d00"]
|
||||
|
||||
download -> bootstrap
|
||||
bootstrap -> init
|
||||
init -> handler [label="event arrives" color="#0066ff"]
|
||||
handler -> respond
|
||||
respond -> freeze
|
||||
|
||||
freeze -> thaw [label="next event" color="#00c853"]
|
||||
thaw -> handler [label="reuse env" color="#00c853"]
|
||||
reuse -> handler [style=dotted color="#00c853"]
|
||||
|
||||
freeze -> shutdown [label="idle too long" style=dashed color="#ff3d00"]
|
||||
}
|
||||
159
docs/graphs/lifecycle.svg
Normal file
159
docs/graphs/lifecycle.svg
Normal file
@@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: lifecycle Pages: 1 -->
|
||||
<svg width="1215pt" height="546pt"
|
||||
viewBox="0.00 0.00 1215.00 546.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 541.5)">
|
||||
<title>lifecycle</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-541.5 1211.25,-541.5 1211.25,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="603.62" y="-518.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Lambda execution environment lifecycle</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_cold</title>
|
||||
<polygon fill="#0a0e17" stroke="#ff3d00" stroke-dasharray="5,2" points="8,-202.25 8,-502 518,-502 518,-202.25 8,-202.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="263" y="-482.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#ff3d00">Cold start (first invocation on a fresh execution environment)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_invoke</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="855,-8 855,-173 1019,-173 1019,-8 855,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="937" y="-153.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Invocation</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_warm</title>
|
||||
<polygon fill="#0a0e17" stroke="#00c853" stroke-dasharray="5,2" points="526,-215.75 526,-307.75 1061,-307.75 1061,-215.75 526,-215.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="793.5" y="-288.55" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#00c853">Warm reuse (subsequent invocations on the same environment)</text>
|
||||
</g>
|
||||
<!-- download -->
|
||||
<g id="node1" class="node">
|
||||
<title>download</title>
|
||||
<polygon fill="#243056" stroke="#1e2a4a" points="503.12,-466.5 370.88,-466.5 370.88,-430.5 503.12,-430.5 503.12,-466.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-451.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">1. Download code</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-438.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">zip / container layers</text>
|
||||
</g>
|
||||
<!-- bootstrap -->
|
||||
<g id="node2" class="node">
|
||||
<title>bootstrap</title>
|
||||
<polygon fill="#243056" stroke="#1e2a4a" points="505.75,-387.25 368.25,-387.25 368.25,-351.25 505.75,-351.25 505.75,-387.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-372.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">2. Start runtime</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-358.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">bootstrap (python3.x)</text>
|
||||
</g>
|
||||
<!-- download->bootstrap -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>download->bootstrap</title>
|
||||
<path fill="none" stroke="#4a5568" d="M437,-430.36C437,-421.12 437,-409.48 437,-398.91"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="440.5,-399.13 437,-389.13 433.5,-399.13 440.5,-399.13"/>
|
||||
</g>
|
||||
<!-- init -->
|
||||
<g id="node3" class="node">
|
||||
<title>init</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="510.25,-285.75 363.75,-285.75 363.75,-210.25 510.25,-210.25 510.25,-285.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-271.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">3. Init phase</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-257.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">run module-level code</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-244.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">import boto3 / aioboto3</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-230.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">build clients</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-217.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(billed; capped at 10 s)</text>
|
||||
</g>
|
||||
<!-- bootstrap->init -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>bootstrap->init</title>
|
||||
<path fill="none" stroke="#4a5568" d="M437,-350.91C437,-336.89 437,-316.46 437,-297.56"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="440.5,-297.57 437,-287.57 433.5,-297.57 440.5,-297.57"/>
|
||||
</g>
|
||||
<!-- handler -->
|
||||
<g id="node4" class="node">
|
||||
<title>handler</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="1010.62,-137.5 863.38,-137.5 863.38,-89 1010.62,-89 1010.62,-137.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="937" y="-123.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">handler(event, context)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="937" y="-109.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">your code runs</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="937" y="-96.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(billed)</text>
|
||||
</g>
|
||||
<!-- init->handler -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>init->handler</title>
|
||||
<path fill="none" stroke="#0066ff" d="M503.5,-209.81C509.66,-207.04 515.88,-204.46 522,-202.25 632.54,-162.3 766.53,-137.96 851.78,-125.3"/>
|
||||
<polygon fill="#0066ff" stroke="#0066ff" points="852.08,-128.79 861.47,-123.88 851.06,-121.86 852.08,-128.79"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="605.96" y="-183.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">event arrives</text>
|
||||
</g>
|
||||
<!-- respond -->
|
||||
<g id="node5" class="node">
|
||||
<title>respond</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="996,-52 908,-52 908,-16 996,-16 996,-52"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="952" y="-30.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">return / raise</text>
|
||||
</g>
|
||||
<!-- handler->respond -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>handler->respond</title>
|
||||
<path fill="none" stroke="#4a5568" d="M941.58,-88.65C943.13,-80.69 944.87,-71.72 946.48,-63.42"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="949.88,-64.29 948.35,-53.81 943.01,-62.96 949.88,-64.29"/>
|
||||
</g>
|
||||
<!-- freeze -->
|
||||
<g id="node8" class="node">
|
||||
<title>freeze</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="1193.88,-393.5 1054.12,-393.5 1054.12,-345 1193.88,-345 1193.88,-393.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-379.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">freeze</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-365.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">process paused</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-352.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(after handler returns)</text>
|
||||
</g>
|
||||
<!-- respond->freeze -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>respond->freeze</title>
|
||||
<path fill="none" stroke="#4a5568" d="M996.29,-51.39C1052.72,-74.98 1147.76,-124.41 1188,-202.25 1221.33,-266.72 1205.08,-295.28 1184,-327 1181.66,-330.52 1178.9,-333.83 1175.88,-336.92"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1173.59,-334.27 1168.5,-343.56 1178.27,-339.48 1173.59,-334.27"/>
|
||||
</g>
|
||||
<!-- thaw -->
|
||||
<g id="node6" class="node">
|
||||
<title>thaw</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="1050.38,-266 949.62,-266 949.62,-230 1050.38,-230 1050.38,-266"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1000" y="-251.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">thaw</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1000" y="-237.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(microseconds)</text>
|
||||
</g>
|
||||
<!-- thaw->handler -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>thaw->handler</title>
|
||||
<path fill="none" stroke="#00c853" d="M991.76,-229.65C981.97,-209.01 965.41,-174.11 953.05,-148.08"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="956.31,-146.78 948.86,-139.25 949.99,-149.78 956.31,-146.78"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="994.4" y="-183.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">reuse env</text>
|
||||
</g>
|
||||
<!-- reuse -->
|
||||
<g id="node7" class="node">
|
||||
<title>reuse</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="925.62,-272.25 814.38,-272.25 814.38,-223.75 931.62,-223.75 931.62,-266.25 925.62,-272.25"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="925.62,-272.25 925.62,-266.25"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="931.62,-266.25 925.62,-266.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="873" y="-257.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">globals retained:</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="873" y="-244.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">clients, /tmp,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="873" y="-230.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">in-memory caches</text>
|
||||
</g>
|
||||
<!-- reuse->handler -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>reuse->handler</title>
|
||||
<path fill="none" stroke="#00c853" stroke-dasharray="1,5" d="M884.44,-223.27C894.54,-202.31 909.39,-171.53 920.72,-148.01"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="923.81,-149.67 925,-139.14 917.5,-146.63 923.81,-149.67"/>
|
||||
</g>
|
||||
<!-- freeze->thaw -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>freeze->thaw</title>
|
||||
<path fill="none" stroke="#00c853" d="M1092.05,-344.65C1085.01,-339.08 1077.73,-333 1071.25,-327 1053.59,-310.64 1035.31,-290.56 1021.69,-274.86"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="1024.4,-272.65 1015.23,-267.34 1019.09,-277.21 1024.4,-272.65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1095.62" y="-318.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">next event</text>
|
||||
</g>
|
||||
<!-- shutdown -->
|
||||
<g id="node9" class="node">
|
||||
<title>shutdown</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="1179.25,-279 1068.75,-279 1068.75,-217 1179.25,-217 1179.25,-279"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-264.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ff3d00">shutdown</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-251.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ff3d00">idle ~5–15 min →</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-237.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ff3d00">env torn down</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-224.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ff3d00">/tmp gone</text>
|
||||
</g>
|
||||
<!-- freeze->shutdown -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>freeze->shutdown</title>
|
||||
<path fill="none" stroke="#ff3d00" stroke-dasharray="5,2" d="M1124,-344.69C1124,-329.32 1124,-308.83 1124,-290.76"/>
|
||||
<polygon fill="#ff3d00" stroke="#ff3d00" points="1127.5,-290.82 1124,-280.82 1120.5,-290.82 1127.5,-290.82"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1151.75" y="-318.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">idle too long</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
70
docs/graphs/system_overview.dot
Normal file
70
docs/graphs/system_overview.dot
Normal file
@@ -0,0 +1,70 @@
|
||||
digraph system_overview {
|
||||
rankdir=LR
|
||||
bgcolor="#0a0e17"
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0"]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
||||
|
||||
label="Sample app — Lambda + MinIO sandbox"
|
||||
labelloc=t
|
||||
fontsize=16
|
||||
fontcolor="#0066ff"
|
||||
|
||||
subgraph cluster_caller {
|
||||
label="Caller"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
invoke [label="invoke.py\n(local) /\nAPI Gateway,\nS3 event,\nStep Functions\n(real AWS)" fillcolor="#243056" shape=box]
|
||||
}
|
||||
|
||||
subgraph cluster_lambda {
|
||||
label="Lambda execution environment"
|
||||
style=dashed
|
||||
color="#0066ff"
|
||||
fontcolor="#0066ff"
|
||||
|
||||
handler [label="handler(event, context)\nlambda_function.py" fillcolor="#1a1a3a" shape=box]
|
||||
|
||||
subgraph cluster_async {
|
||||
label="asyncio.Queue producer / consumer"
|
||||
style=dotted
|
||||
color="#0066ff"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
producer [label="producer\nlist_objects_v2 (paginator)\nfilter *.pdf" fillcolor="#0d1a33" shape=box]
|
||||
queue [label="asyncio.Queue\nmaxsize=2000\n(backpressure)" fillcolor="#121829" shape=cylinder]
|
||||
consumer [label="consumer\ngenerate_presigned_url\nappend JSONL" fillcolor="#0d1a33" shape=box]
|
||||
}
|
||||
|
||||
tmp [label="/tmp/<uuid>.jsonl\nstreamed manifest\n(ephemeral, 512 MB default)" fillcolor="#121829" shape=cylinder fontcolor="#ffc107"]
|
||||
}
|
||||
|
||||
subgraph cluster_storage {
|
||||
label="Object storage"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
minio [label="MinIO (local)\nor real S3" fillcolor="#1a3a1a" shape=cylinder fontcolor="#00c853"]
|
||||
bucket [label="my-company-reports-bucket\n2026/04/*.pdf\nmanifests/<uuid>.jsonl" fillcolor="#121829" shape=folder]
|
||||
}
|
||||
|
||||
response [label="response\n{count, manifest_key,\nmanifest_url}\n(< 1 KB; sidesteps 6 MB cap)" fillcolor="#243056" shape=note fontcolor="#00c853"]
|
||||
|
||||
invoke -> handler [label="event"]
|
||||
handler -> producer [label="spawn task"]
|
||||
handler -> consumer [label="spawn task"]
|
||||
|
||||
producer -> minio [label="LIST"]
|
||||
minio -> producer [label="page (1000 keys)" style=dashed]
|
||||
producer -> queue [label="key" color="#0066ff"]
|
||||
queue -> consumer [label="key"]
|
||||
consumer -> minio [label="presign\n(local HMAC)" style=dotted]
|
||||
consumer -> tmp [label="JSONL line"]
|
||||
|
||||
tmp -> minio [label="put_object\nmanifests/<uuid>.jsonl"]
|
||||
handler -> response [label="return"]
|
||||
minio -> bucket [style=invis]
|
||||
}
|
||||
193
docs/graphs/system_overview.svg
Normal file
193
docs/graphs/system_overview.svg
Normal file
@@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: system_overview Pages: 1 -->
|
||||
<svg width="1531pt" height="306pt"
|
||||
viewBox="0.00 0.00 1531.00 306.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 301.5)">
|
||||
<title>system_overview</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-301.5 1526.75,-301.5 1526.75,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="761.38" y="-278.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Sample app — Lambda + MinIO sandbox</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_caller</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-95 8,-228 121,-228 121,-95 8,-95"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-208.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Caller</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_lambda</title>
|
||||
<polygon fill="#0a0e17" stroke="#0066ff" stroke-dasharray="5,2" points="166.5,-108 166.5,-262 1308,-262 1308,-108 166.5,-108"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="737.25" y="-242.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Lambda execution environment</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_async</title>
|
||||
<polygon fill="#0a0e17" stroke="#0066ff" stroke-dasharray="1,5" points="409,-116 409,-226 1040.75,-226 1040.75,-116 409,-116"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="724.88" y="-206.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">asyncio.Queue producer / consumer</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_storage</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="1162,-8 1162,-100 1514.75,-100 1514.75,-8 1162,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1338.38" y="-80.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Object storage</text>
|
||||
</g>
|
||||
<!-- invoke -->
|
||||
<g id="node1" class="node">
|
||||
<title>invoke</title>
|
||||
<polygon fill="#243056" stroke="#1e2a4a" points="113,-192.5 16,-192.5 16,-103.5 113,-103.5 113,-192.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-178.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">invoke.py</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-164.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(local) /</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-151.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">API Gateway,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-137.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">S3 event,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-124.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">Step Functions</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-110.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(real AWS)</text>
|
||||
</g>
|
||||
<!-- handler -->
|
||||
<g id="node2" class="node">
|
||||
<title>handler</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="321.75,-166 174.5,-166 174.5,-130 321.75,-130 321.75,-166"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="248.12" y="-151.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">handler(event, context)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="248.12" y="-137.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">lambda_function.py</text>
|
||||
</g>
|
||||
<!-- invoke->handler -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>invoke->handler</title>
|
||||
<path fill="none" stroke="#4a5568" d="M113.22,-148C128.48,-148 145.85,-148 162.92,-148"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="162.54,-151.5 172.54,-148 162.54,-144.5 162.54,-151.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="143.75" y="-150.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">event</text>
|
||||
</g>
|
||||
<!-- producer -->
|
||||
<g id="node3" class="node">
|
||||
<title>producer</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="578.5,-172.25 417,-172.25 417,-123.75 578.5,-123.75 578.5,-172.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-157.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">producer</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-144.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">list_objects_v2 (paginator)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-130.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">filter *.pdf</text>
|
||||
</g>
|
||||
<!-- handler->producer -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>handler->producer</title>
|
||||
<path fill="none" stroke="#4a5568" d="M322.08,-148C348.16,-148 377.89,-148 405.33,-148"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="405.01,-151.5 415.01,-148 405.01,-144.5 405.01,-151.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="365.25" y="-150.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">spawn task</text>
|
||||
</g>
|
||||
<!-- consumer -->
|
||||
<g id="node5" class="node">
|
||||
<title>consumer</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="1032.75,-181.25 888.5,-181.25 888.5,-132.75 1032.75,-132.75 1032.75,-181.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="960.62" y="-166.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">consumer</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="960.62" y="-153.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">generate_presigned_url</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="960.62" y="-139.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">append JSONL</text>
|
||||
</g>
|
||||
<!-- handler->consumer -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>handler->consumer</title>
|
||||
<path fill="none" stroke="#4a5568" d="M321.87,-165.21C349,-171.06 380.15,-177.09 408.75,-181 569.01,-202.91 611.48,-216.8 772.25,-199 807.08,-195.14 844.9,-187.37 877.38,-179.55"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="877.83,-183.05 886.71,-177.27 876.16,-176.25 877.83,-183.05"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="630.25" y="-209.83" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">spawn task</text>
|
||||
</g>
|
||||
<!-- response -->
|
||||
<g id="node9" class="node">
|
||||
<title>response</title>
|
||||
<polygon fill="#243056" stroke="#1e2a4a" points="580.75,-100 408.75,-100 408.75,-38 586.75,-38 586.75,-94 580.75,-100"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="580.75,-100 580.75,-94"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="586.75,-94 580.75,-94"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-85.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">response</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-72.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">{count, manifest_key,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-58.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">manifest_url}</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-45.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">(< 1 KB; sidesteps 6 MB cap)</text>
|
||||
</g>
|
||||
<!-- handler->response -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>handler->response</title>
|
||||
<path fill="none" stroke="#4a5568" d="M306.87,-129.58C333.84,-120.97 366.73,-110.48 397.51,-100.66"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="398.4,-104.05 406.86,-97.68 396.27,-97.38 398.4,-104.05"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="365.25" y="-120.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">return</text>
|
||||
</g>
|
||||
<!-- queue -->
|
||||
<g id="node4" class="node">
|
||||
<title>queue</title>
|
||||
<path fill="#121829" stroke="#1e2a4a" d="M772.25,-184.28C772.25,-187.63 750.18,-190.34 723,-190.34 695.82,-190.34 673.75,-187.63 673.75,-184.28 673.75,-184.28 673.75,-129.72 673.75,-129.72 673.75,-126.37 695.82,-123.66 723,-123.66 750.18,-123.66 772.25,-126.37 772.25,-129.72 772.25,-129.72 772.25,-184.28 772.25,-184.28"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M772.25,-184.28C772.25,-180.94 750.18,-178.22 723,-178.22 695.82,-178.22 673.75,-180.94 673.75,-184.28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="723" y="-166.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">asyncio.Queue</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="723" y="-153.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">maxsize=2000</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="723" y="-139.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(backpressure)</text>
|
||||
</g>
|
||||
<!-- producer->queue -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>producer->queue</title>
|
||||
<path fill="none" stroke="#0066ff" d="M578.66,-148.22C603.39,-148.6 630.71,-149.35 655.75,-150.75 657.91,-150.87 660.11,-151.01 662.34,-151.16"/>
|
||||
<polygon fill="#0066ff" stroke="#0066ff" points="661.8,-154.63 672.04,-151.88 662.33,-147.65 661.8,-154.63"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="630.25" y="-153.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">key</text>
|
||||
</g>
|
||||
<!-- minio -->
|
||||
<g id="node7" class="node">
|
||||
<title>minio</title>
|
||||
<path fill="#1a3a1a" stroke="#1e2a4a" d="M1255.75,-59.69C1255.75,-62.1 1236.53,-64.06 1212.88,-64.06 1189.22,-64.06 1170,-62.1 1170,-59.69 1170,-59.69 1170,-20.31 1170,-20.31 1170,-17.9 1189.22,-15.94 1212.88,-15.94 1236.53,-15.94 1255.75,-17.9 1255.75,-20.31 1255.75,-20.31 1255.75,-59.69 1255.75,-59.69"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M1255.75,-59.69C1255.75,-57.27 1236.53,-55.31 1212.88,-55.31 1189.22,-55.31 1170,-57.27 1170,-59.69"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-43.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">MinIO (local)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-29.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">or real S3</text>
|
||||
</g>
|
||||
<!-- producer->minio -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>producer->minio</title>
|
||||
<path fill="none" stroke="#4a5568" d="M578.76,-132.08C608.53,-126.37 642.63,-120.1 673.75,-115 721.03,-107.25 1032.17,-64.57 1158.6,-47.28"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1158.95,-50.76 1168.39,-45.94 1158.01,-43.83 1158.95,-50.76"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="830.38" y="-100.82" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">LIST</text>
|
||||
</g>
|
||||
<!-- queue->consumer -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>queue->consumer</title>
|
||||
<path fill="none" stroke="#4a5568" d="M772.48,-155.98C778.47,-155.89 784.5,-155.8 790.25,-155.75 825.92,-155.41 834.83,-155.5 870.5,-155.75 872.56,-155.76 874.64,-155.78 876.74,-155.8"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="876.57,-159.3 886.6,-155.89 876.64,-152.3 876.57,-159.3"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="830.38" y="-158.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">key</text>
|
||||
</g>
|
||||
<!-- tmp -->
|
||||
<g id="node6" class="node">
|
||||
<title>tmp</title>
|
||||
<path fill="#121829" stroke="#1e2a4a" d="M1300,-180.28C1300,-183.63 1260.95,-186.34 1212.88,-186.34 1164.8,-186.34 1125.75,-183.63 1125.75,-180.28 1125.75,-180.28 1125.75,-125.72 1125.75,-125.72 1125.75,-122.37 1164.8,-119.66 1212.88,-119.66 1260.95,-119.66 1300,-122.37 1300,-125.72 1300,-125.72 1300,-180.28 1300,-180.28"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M1300,-180.28C1300,-176.94 1260.95,-174.22 1212.88,-174.22 1164.8,-174.22 1125.75,-176.94 1125.75,-180.28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-162.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">/tmp/<uuid>.jsonl</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-149.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">streamed manifest</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-135.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(ephemeral, 512 MB default)</text>
|
||||
</g>
|
||||
<!-- consumer->tmp -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>consumer->tmp</title>
|
||||
<path fill="none" stroke="#4a5568" d="M1033,-155.86C1058.16,-155.46 1086.89,-155 1113.86,-154.57"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1113.84,-158.07 1123.78,-154.41 1113.73,-151.07 1113.84,-158.07"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1079.25" y="-158.18" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">JSONL line</text>
|
||||
</g>
|
||||
<!-- consumer->minio -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>consumer->minio</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="1,5" d="M992.03,-132.46C1008.5,-120.4 1029.72,-106.73 1050.75,-98.5 1074.67,-89.14 1083.23,-96.65 1107.75,-89 1125.6,-83.43 1144.31,-75.39 1160.87,-67.42"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1162.09,-70.72 1169.52,-63.16 1159,-64.44 1162.09,-70.72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1079.25" y="-112.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">presign</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1079.25" y="-101.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">(local HMAC)</text>
|
||||
</g>
|
||||
<!-- tmp->minio -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>tmp->minio</title>
|
||||
<path fill="none" stroke="#4a5568" d="M1212.88,-119.53C1212.88,-105.74 1212.88,-89.75 1212.88,-75.73"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1216.38,-76.03 1212.88,-66.03 1209.38,-76.03 1216.38,-76.03"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1198.62" y="-94.56" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">put_object</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1198.62" y="-83.31" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">manifests/<uuid>.jsonl</text>
|
||||
</g>
|
||||
<!-- minio->producer -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>minio->producer</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="5,2" d="M1169.68,-40.78C1093.42,-42.74 927.8,-49.54 790.25,-72.75 738.1,-81.55 725.32,-85.28 673.75,-97 642.95,-104 635.14,-105.39 604.75,-114 598.1,-115.88 591.24,-117.9 584.35,-119.99"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="583.53,-116.58 574.99,-122.86 585.58,-123.27 583.53,-116.58"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="830.38" y="-75.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">page (1000 keys)</text>
|
||||
</g>
|
||||
<!-- bucket -->
|
||||
<g id="node8" class="node">
|
||||
<title>bucket</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="1506.75,-64.25 1503.75,-68.25 1482.75,-68.25 1479.75,-64.25 1337,-64.25 1337,-15.75 1506.75,-15.75 1506.75,-64.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1421.88" y="-49.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">my-company-reports-bucket</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1421.88" y="-36.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">2026/04/*.pdf</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1421.88" y="-22.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">manifests/<uuid>.jsonl</text>
|
||||
</g>
|
||||
<!-- minio->bucket -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
1612
docs/index.html
Normal file
1612
docs/index.html
Normal file
File diff suppressed because it is too large
Load Diff
30
docs/lambdas-md/lambda-01-overview.md
Normal file
30
docs/lambdas-md/lambda-01-overview.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Overview
|
||||
|
||||
> A study site built on top of a working Lambda + MinIO sandbox. Read the page, run the code, break things on purpose.
|
||||
|
||||
## What this is
|
||||
|
||||
The repo at the root of this site (`ethics/`) holds a Python AWS Lambda function — `lambda_function.py` — that lists PDFs in an S3 bucket under a prefix, paginates, generates 15-minute presigned URLs, and writes a JSONL manifest. It runs locally against MinIO via `docker compose`, with the same handler signature as a real Lambda. This site explains the surrounding mental model in the order you'd want to study it before walking into a Lambda-heavy interview or production rotation.
|
||||
|
||||
## How it's organised
|
||||
|
||||
The sidebar groups topics into four reading orders. **Foundations** is the picture in your head. **Operating** covers the day-to-day knobs. **Production** covers what changes when real users and real money are involved. **Reference** holds the must-know checklist ([Pitfalls](lambda-16-pitfalls.md)), brief orientations on adjacent tools ([Glue, Prometheus/Grafana](lambda-17-adjacent.md)), the hands-on labs ([Labs](lambda-18-labs.md)), and the repo tree ([Repository](lambda-19-repository.md)).
|
||||
|
||||
## How to use it
|
||||
|
||||
1. **Read top-to-bottom** — the order in the sidebar is the recommended study path.
|
||||
2. **Run the sandbox.** `make install && make up && SOURCE_DIR=<dir> make seed && make invoke`. The handler executes locally against MinIO; you can break it without burning AWS credit.
|
||||
3. **Do the labs.** Each one mutates the existing app: deploy to real AWS, add an S3 trigger, switch to arm64, enable Provisioned Concurrency, fan out across prefixes with Step Functions, and so on.
|
||||
4. **Skim Pitfalls** the night before any interview or design review.
|
||||
|
||||
## System overview
|
||||
|
||||
> Caller → handler → MinIO/S3 → manifest write-back. The async producer/consumer overlaps S3 LIST calls with presigning + JSONL writes, so the manifest streams to `/tmp` rather than buffering in memory.
|
||||
|
||||

|
||||
|
||||
**Legend:**
|
||||
- 🟢 Real / live
|
||||
- 🟡 Ephemeral / caveat
|
||||
- 🔵 Lambda boundary
|
||||
- 🔴 Pitfall
|
||||
48
docs/lambdas-md/lambda-02-mental-model.md
Normal file
48
docs/lambdas-md/lambda-02-mental-model.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Mental Model
|
||||
|
||||
> Lambda is a Linux process whose lifecycle is managed for you. Most of the surprise comes from forgetting that it's still a process.
|
||||
|
||||
## What Lambda actually is
|
||||
|
||||
Each invocation runs inside an **execution environment**: a Firecracker microVM running the Lambda runtime (e.g. `python3.13`), with your code unpacked into `/var/task` and an ephemeral `/tmp`. AWS owns the VM; you own everything inside the process. The microVM is created on demand, kept warm for a while, then torn down when idle traffic stops feeding it. You don't pick a server, but there *is* a server, and it has memory, a clock, and a filesystem.
|
||||
|
||||
## The two phases
|
||||
|
||||
Every cold start splits cleanly into two:
|
||||
|
||||
- **Init phase** — your module-level code runs once: imports, client construction, anything outside the handler function. Capped at 10 s. Billed at full configured memory. The `os.environ` reads at the top of `lambda_function.py` happen here.
|
||||
- **Handler phase** — `handler(event, context)` runs once per invocation. Billed per-millisecond at configured memory. Subsequent invocations on the same environment skip the init phase and go straight here.
|
||||
|
||||
This split is the single most useful thing to internalise. Heavy work at module level → pay it once per cold start. Heavy work inside the handler → pay it every invocation.
|
||||
|
||||
## Globals persist across warm invocations
|
||||
|
||||
Anything assigned at module scope survives between handler calls on the same environment. That includes the boto3 client (good — connection reuse, TCP keep-alive, no re-handshake) and any in-memory cache you build (good — but be careful, see Pitfalls). It also includes mutations you didn't mean to keep, like a list you appended to without thinking. The same warm container can serve thousands of invocations in a row, then disappear.
|
||||
|
||||
```python
|
||||
# module level — runs once per cold start, reused across warm invocations
|
||||
BUCKET = os.environ["BUCKET_NAME"]
|
||||
ENDPOINT = os.environ.get("S3_ENDPOINT_URL")
|
||||
|
||||
# handler level — runs every invocation
|
||||
def handler(event, context):
|
||||
return asyncio.run(_run())
|
||||
```
|
||||
|
||||
## /tmp is real but local
|
||||
|
||||
Each environment has its own `/tmp` (default 512 MB, configurable to 10 GB). It persists across warm invocations on that environment, so you can stash artefacts you'd rather not rebuild — but it is **not** shared between concurrent executions, and it's gone when the environment dies. `lambda_function.py` writes `/tmp/<uuid>.jsonl` per invocation and uploads it to S3 at the end; the file then becomes garbage, and the next invocation starts fresh.
|
||||
|
||||
## Concurrency is horizontal
|
||||
|
||||
If two events arrive while one is being processed, AWS spins up a second execution environment. Each environment processes one invocation at a time, single-threaded relative to your handler. The "concurrency" you see in CloudWatch is the count of environments running in parallel. There is no thread pool to tune. There is no shared memory between environments. If you need shared state, externalise it (DynamoDB, Redis, S3).
|
||||
|
||||
## The reuse window
|
||||
|
||||
Idle environments stick around for roughly 5–15 minutes (AWS doesn't promise a number) before being recycled. That's why a function that sees one request a minute almost never cold-starts, and a function that sees one a day always does. [Cold Starts](lambda-04-cold-starts.md) covers what that costs and how to mitigate it.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
> Init is paid once, handler is paid every time. Freeze/thaw is free. Shutdown happens when nobody's looking.
|
||||
|
||||

|
||||
52
docs/lambdas-md/lambda-03-limits.md
Normal file
52
docs/lambdas-md/lambda-03-limits.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Limits — Cheatsheet
|
||||
|
||||
> Every number worth memorising. The "why it matters" column is the part interviews actually probe.
|
||||
|
||||
## Per-function compute & storage
|
||||
|
||||
| Limit | Default | Max | Why it matters |
|
||||
|-------|---------|-----|----------------|
|
||||
| Memory | 128 MB | 10 240 MB | CPU scales linearly with memory. More memory ≠ just more headroom — at >1769 MB you get a full vCPU; at higher tiers, multiple. Often *cheaper* to bump memory because duration drops faster than cost rises. |
|
||||
| Timeout | 3 s | 900 s (15 min) | 3 s default is too short for almost anything that talks to S3. Set explicitly; don't accept the default. API Gateway caps at 29 s no matter what your function says (see below). |
|
||||
| Ephemeral storage (/tmp) | 512 MB | 10 240 MB | Persists across warm invocations on the same env, vanishes on cold start. Not shared between concurrent envs. Pay per-invocation for >512 MB. |
|
||||
| Init phase | 10 s hard cap | 10 s hard cap | Module-level code (imports, client construction). Heavy ML model loads, custom JIT warmups — measure them or you'll trip this. |
|
||||
|
||||
## Payloads & responses
|
||||
|
||||
| Limit | Value | Why it matters |
|
||||
|-------|-------|----------------|
|
||||
| Sync invocation request | 6 MB | Hard cap on the event body for `RequestResponse` invocations. |
|
||||
| Sync invocation response | 6 MB | Truncated silently above this — your handler "succeeds" but the caller gets a 413. `lambda_function.py` sidesteps this by returning a manifest URL instead of inlining all presigned URLs. |
|
||||
| Async invocation event | 256 KB | For `Event` invocations and most event-source-mapped triggers (S3, EventBridge, SNS). |
|
||||
| Response streaming | 20 MB (soft) / unlimited (with bandwidth cap) | Function URLs and Lambda Streaming response mode break the 6 MB cap by flushing chunks. Not all clients/SDKs support it. |
|
||||
| Environment variables | 4 KB total | Per function, all keys+values combined. Big config → Parameter Store / Secrets Manager. |
|
||||
| Event size (SQS, SNS, EventBridge) | 256 KB each | Producer-side limit. Larger payloads → store in S3, send a pointer. |
|
||||
|
||||
## Packaging
|
||||
|
||||
| Limit | Value | Why it matters |
|
||||
|-------|-------|----------------|
|
||||
| Zip upload (direct) | 50 MB | Above this you must upload via S3 first. |
|
||||
| Zip unzipped (function + layers) | 250 MB | Total of `/var/task` + all layers extracted. `aioboto3`+deps is ~50 MB; you have headroom but not infinite. |
|
||||
| Container image | 10 GB | Per image. Preferred when you'd otherwise blow the 250 MB zip ceiling — e.g. ML deps with native binaries. |
|
||||
| Layers | 5 per function | Ordering matters: later layers overwrite earlier. Layers count toward the 250 MB unzipped cap. |
|
||||
|
||||
## Concurrency & scaling
|
||||
|
||||
| Limit | Default | Notes |
|
||||
|-------|---------|-------|
|
||||
| Account concurrent executions | 1 000 / region | Soft quota — request increase via Service Quotas. The single most common throttling cause in production. |
|
||||
| Burst concurrency | 500–3 000 (region-dependent) | How many fresh environments AWS will spin up immediately at traffic spike. Beyond this, scale-up is +500 envs / min. |
|
||||
| Reserved concurrency | 0 to account quota | Carves a slice of the account pool for a function. Setting it to 0 effectively disables the function. |
|
||||
| Provisioned concurrency | 0 by default | Pre-warmed envs. Eliminates cold starts at the cost of paying for idle capacity. Bills as PC-seconds + invocation cost. |
|
||||
|
||||
## Time & rate limits at the edges
|
||||
|
||||
| Surface | Limit | Why it matters |
|
||||
|---------|-------|----------------|
|
||||
| API Gateway integration timeout | 29 s | Caps your effective Lambda timeout when fronted by API GW, regardless of what the Lambda timeout says. Function URLs allow up to 15 min. |
|
||||
| Async invocation event age | 6 h | If retries don't succeed in this window, the event is dropped (or sent to DLQ / on-failure destination). |
|
||||
| Async retry attempts | 2 (default) | Total of 3 attempts (initial + 2). Configurable down to 0. |
|
||||
| SQS visibility timeout requirement | ≥ 6× function timeout | AWS recommendation. Otherwise messages reappear while still being processed. |
|
||||
|
||||
> **Memorisation hack.** Three numbers cover most interview questions: **15 minutes** (timeout), **10 GB** (memory and /tmp ceiling), **6 MB** (sync payload). Everything else is a footnote until you hit a specific design.
|
||||
44
docs/lambdas-md/lambda-04-cold-starts.md
Normal file
44
docs/lambdas-md/lambda-04-cold-starts.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Cold Starts
|
||||
|
||||
> Init Duration vs warm path. Mitigations: Provisioned Concurrency, arm64, lazy imports, smaller packages, SnapStart.
|
||||
|
||||

|
||||
|
||||
## What triggers a cold start
|
||||
|
||||
A cold start happens whenever Lambda must create a new execution environment: the very first request after a deployment, when traffic spikes beyond the number of warm environments, and after an environment has been idle long enough to be recycled (typically 5–15 minutes, unspecified by AWS). Deployments always cold-start the incoming version — you can't avoid the first one, only reduce how long it takes.
|
||||
|
||||
## The cold path
|
||||
|
||||
AWS provisions a Firecracker microVM, downloads and unpacks your code (or pulls the container image), starts the language runtime, then runs your module-level code. Only after all of that does your handler function get called. The timeline is roughly:
|
||||
|
||||
1. **Environment provisioning** — microVM boot, network attachment, filesystem mount. Not billed; AWS absorbs this.
|
||||
2. **Init phase** — your module-level code: imports, client construction, config reads. Billed at full configured memory. Capped at 10 s.
|
||||
3. **Handler phase** — `handler(event, context)` runs. Billed per-ms.
|
||||
|
||||
CloudWatch shows this split: the `REPORT` line includes `Init Duration` only on cold invocations. Warm invocations have no `Init Duration` line.
|
||||
|
||||
## Typical numbers
|
||||
|
||||
| Runtime | Typical cold start (p50) | Typical cold start (p99) |
|
||||
|---------|--------------------------|--------------------------|
|
||||
| Python 3.13 (zip, minimal deps) | ~150 ms | ~400 ms |
|
||||
| Python 3.13 (zip, aioboto3 + aiofiles) | ~300 ms | ~700 ms |
|
||||
| Node.js 22 | ~100 ms | ~300 ms |
|
||||
| Java 21 (without SnapStart) | ~1–2 s | ~3–5 s |
|
||||
| Java 21 (SnapStart enabled) | ~200 ms | ~600 ms |
|
||||
| Container image (any runtime) | +100–300 ms | first pull can be 1–3 s |
|
||||
|
||||
## Mitigations
|
||||
|
||||
**Provisioned Concurrency (PC)** — pre-warms N environments so they're always in the "warm" state. Eliminates cold starts for the provisioned slots. You pay for those slots 24/7 even when idle. Use for latency-sensitive, predictable-traffic paths. Schedule PC changes via Application Auto Scaling for cost efficiency.
|
||||
|
||||
**arm64** — Graviton2 executes the init phase ~10% faster than x86_64 for CPU-bound init work. Combined with the ~20% price reduction, arm64 is the default choice unless native wheels block you.
|
||||
|
||||
**Smaller packages** — Lambda downloads and unpacks your zip on every cold start. Trimming unused transitive dependencies (use `pip install --no-deps` audit or `pipdeptree`) and stripping test/doc files shaves real time. Every MB of extracted code costs a few ms.
|
||||
|
||||
**Lazy imports** — move rarely-used or slow imports inside the handler (or into a lazy-init guard). The most common win is heavy ML libraries only needed for inference: import them on first call, cache the result in a module-level variable.
|
||||
|
||||
**SnapStart (Java only)** — takes a snapshot of the initialised JVM state after your init phase, then restores from that snapshot on cold starts. Collapses 1–5 s JVM startup to ~200 ms. Not available for Python or Node.
|
||||
|
||||
> **When cold starts don't matter:** batch jobs, async event pipelines, scheduled tasks — nobody is waiting on the p99. Only optimise cold starts when a human is waiting synchronously for the response.
|
||||
35
docs/lambdas-md/lambda-05-concurrency.md
Normal file
35
docs/lambdas-md/lambda-05-concurrency.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Concurrency
|
||||
|
||||
> Account quota, reserved, provisioned. The "100 RPS × 200 ms" math.
|
||||
|
||||
## The fundamental model
|
||||
|
||||
Lambda concurrency = the number of execution environments processing requests at the same instant. Each environment handles exactly one invocation at a time. There is no thread pool, no event loop shared across invocations — if two requests arrive simultaneously, AWS spins up two separate environments.
|
||||
|
||||
The key formula: **concurrency ≈ RPS × average duration (in seconds)**. At 100 requests/s with a 200 ms average handler duration, you need 100 × 0.2 = **20 concurrent environments**. At 500 ms average, you need 50. At 2 s average, 200 — and so on. Latency optimisation directly reduces your concurrency footprint.
|
||||
|
||||
## Account concurrency pool
|
||||
|
||||
Every AWS account has a regional concurrency quota — default **1 000 concurrent executions** per region, shared across all functions. When the pool is full, new invocations get throttled (sync → HTTP 429 TooManyRequestsException; async → queued and retried). Raising the limit requires a Service Quotas increase request; AWS typically grants up to 10 000 with a business justification.
|
||||
|
||||
This is the single most common production surprise: one function spikes and starves all others in the same region. Reserved concurrency is the fix.
|
||||
|
||||
## Types of concurrency
|
||||
|
||||
| Type | What it does | Cost | Use for |
|
||||
|------|--------------|------|---------|
|
||||
| **Unreserved** | Draws from the shared regional pool on demand | Invocation + duration only | Most functions |
|
||||
| **Reserved** | Carves a slice of the regional pool exclusively for this function; acts as both a floor and a ceiling | No extra charge | Protecting critical paths from noisy neighbours; throttling cost runaway |
|
||||
| **Provisioned** | Pre-warms N environments; they stay initialised 24/7 | PC-hours + invocation | Latency-sensitive functions where cold starts are unacceptable |
|
||||
|
||||
## Reserved concurrency edge cases
|
||||
|
||||
- Setting reserved concurrency to **0** disables the function entirely — useful as a circuit breaker.
|
||||
- Reserved concurrency counts against the account pool even when idle. If you set 500 reserved on a function, only 500 remain for all other functions (at default 1 000).
|
||||
- Reserved concurrency does **not** pre-warm. You still cold-start; you just can't scale past the cap.
|
||||
|
||||
## Burst scaling
|
||||
|
||||
When traffic spikes from zero, Lambda can spin up environments quickly — but not infinitely fast. The burst limit (region-dependent, typically 500–3 000 immediate) is how many environments AWS will create right now. Beyond that, it adds **500 new environments per minute**. A spike from 0 to 5 000 concurrent requests takes several minutes to fully absorb. Provisioned Concurrency or pre-warming via a ping mechanism is the fix for sudden large spikes.
|
||||
|
||||
> **Interview answer template:** "Concurrency = RPS × duration. Default pool is 1 000/region. Reserved carves a slice and prevents both starvation and runaway. Provisioned pre-warms to eliminate cold starts, but you pay for idle capacity."
|
||||
33
docs/lambdas-md/lambda-06-triggers.md
Normal file
33
docs/lambdas-md/lambda-06-triggers.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Triggers
|
||||
|
||||
> Fan-in catalogue: API GW, Function URL, S3, SQS, SNS, EventBridge, DynamoDB streams, Kinesis, ALB, schedule, Step Functions.
|
||||
|
||||
## Three invocation models
|
||||
|
||||
Every trigger falls into one of three models, and the model determines retry behaviour, error handling, and whether the caller can see the response.
|
||||
|
||||
| Model | Caller behaviour | Retries on error | Max event size |
|
||||
|-------|------------------|------------------|----------------|
|
||||
| **Synchronous** | Blocks for response; gets result or error directly | None — caller decides | 6 MB request + response |
|
||||
| **Asynchronous** | Gets 202 immediately; Lambda queues + retries internally | 2 retries (3 total) over up to 6 h | 256 KB event |
|
||||
| **Poll-based (ESM)** | Lambda polls the source on your behalf; batches records | Keeps retrying until success or record expires/goes to DLQ | Depends on source |
|
||||
|
||||
## Trigger catalogue
|
||||
|
||||
| Trigger | Model | Key notes |
|
||||
|---------|-------|-----------|
|
||||
| **API Gateway (REST / HTTP)** | Sync | 29 s integration timeout regardless of Lambda timeout. HTTP API is cheaper and lower-latency than REST API. Transforms request/response. |
|
||||
| **Function URL** | Sync | Direct HTTPS endpoint on the function; no API Gateway layer. Supports up to 15 min timeout and response streaming. Simpler, cheaper, fewer features. |
|
||||
| **ALB (Application Load Balancer)** | Sync | Like API GW but routes at L7; useful when Lambda is one target among EC2/ECS targets. 29 s timeout. |
|
||||
| **S3 event notification** | Async | Fires on object create/delete/etc. At-least-once delivery. Large PUT creates exactly one event per object but notifications can duplicate. Common pattern: S3 → SNS → SQS → Lambda for fan-out + replay. |
|
||||
| **SNS** | Async | Fan-out: one message → multiple subscribers. At-least-once. Dead-letter queue on the subscription, not the topic. |
|
||||
| **EventBridge (CloudWatch Events)** | Async | Event bus with content-based routing rules. Also the managed scheduler (cron/rate expressions, timezone-aware since 2022). At-least-once. |
|
||||
| **SQS** | Poll-based (ESM) | Lambda polls and batches (up to 10 000 msg). Standard: at-least-once, unordered. FIFO: ordered per message group, exactly-once with dedup. Visibility timeout must be ≥ 6× function timeout. Partial batch failure via `batchItemFailures`. |
|
||||
| **Kinesis Data Streams** | Poll-based (ESM) | One Lambda shard per stream shard. Records expire (24 h–1 yr); Lambda retries until success or expiry. Use bisect-on-error and `batchItemFailures` to avoid one bad record blocking an entire shard. |
|
||||
| **DynamoDB Streams** | Poll-based (ESM) | Captures item-level changes. Ordered per partition key. 24 h retention. Same retry behaviour as Kinesis. Use for CDC (change-data-capture) patterns. |
|
||||
| **Step Functions** | Sync (Task state) | Step Functions calls the function synchronously and waits for the result. Retries and timeouts are defined in the state machine, not Lambda. See the [Step Functions](lambda-12-step-functions.md) section. |
|
||||
| **Cognito / SES / IoT etc.** | Sync or Async | Service-specific; check the docs for each. Cognito triggers (pre-signup, pre-token) are sync and block the auth flow. |
|
||||
|
||||
## Choosing between SQS and SNS+SQS
|
||||
|
||||
Use plain **SQS → Lambda** when you have one consumer and want to buffer, batch, and retry. Use **SNS → SQS → Lambda** when you need fan-out (multiple independent consumers each get a copy) or when the producer is an AWS service that speaks SNS natively (S3 event notifications, for example). The SNS layer decouples producers from the queue topology.
|
||||
58
docs/lambdas-md/lambda-07-iam.md
Normal file
58
docs/lambdas-md/lambda-07-iam.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# IAM & Permissions
|
||||
|
||||
> Execution role vs resource policy. The two policies most people confuse.
|
||||
|
||||
## Two independent permission layers
|
||||
|
||||
Lambda has two separate permission surfaces that must each be correct independently. Confusing them is the most common "it works locally but not in AWS" failure.
|
||||
|
||||
| Layer | Question it answers | Who creates it |
|
||||
|-------|---------------------|----------------|
|
||||
| **Execution role** | What can *this Lambda function do* once running? (call S3, write to DynamoDB, publish to SNS…) | You — attached at function creation |
|
||||
| **Resource policy** | Who is *allowed to invoke* this Lambda function? (API Gateway, another account, EventBridge…) | AWS adds it automatically for most triggers; you add it for cross-account or manual grants |
|
||||
|
||||
## Execution role
|
||||
|
||||
The execution role is an IAM role that Lambda assumes when running your function. Every Lambda must have one. The role's attached policies determine what AWS API calls the function can make. At minimum, every function needs:
|
||||
|
||||
```
|
||||
# minimum: write its own logs
|
||||
logs:CreateLogGroup
|
||||
logs:CreateLogStream
|
||||
logs:PutLogEvents
|
||||
```
|
||||
|
||||
Common additions for a function that reads/writes S3:
|
||||
|
||||
```
|
||||
s3:GetObject
|
||||
s3:PutObject
|
||||
s3:ListBucket # needed for paginator; often forgotten
|
||||
kms:Decrypt # if the bucket uses a CMK, this is also required
|
||||
```
|
||||
|
||||
The `AWSLambdaBasicExecutionRole` managed policy covers logs only — it is intentionally minimal. `AWSLambdaVPCAccessExecutionRole` adds the ENI permissions needed when the function is in a VPC.
|
||||
|
||||
## Resource policy
|
||||
|
||||
The resource policy is attached to the Lambda function itself (not an IAM identity). When you add an S3 event notification or API Gateway integration in the console, AWS automatically adds a resource policy entry allowing that service to invoke the function. For cross-account invocations you add this manually via `aws lambda add-permission`.
|
||||
|
||||
```bash
|
||||
# grant another account permission to invoke
|
||||
aws lambda add-permission \
|
||||
--function-name my-function \
|
||||
--principal 123456789012 \ # the other AWS account
|
||||
--action lambda:InvokeFunction \
|
||||
--statement-id cross-account-invoke
|
||||
```
|
||||
|
||||
## Common mistakes
|
||||
|
||||
- **Missing `s3:ListBucket` on the bucket resource.** `ListObjectsV2` requires this on the *bucket ARN* (not the object ARN). Forgetting it causes AccessDenied on the paginator even when GetObject works fine.
|
||||
- **Wrong resource ARN scope.** `s3:GetObject` must be on `arn:aws:s3:::bucket-name/*`; `s3:ListBucket` must be on `arn:aws:s3:::bucket-name`. Swapping them is a frequent typo.
|
||||
- **CMK not in execution role.** KMS-encrypted bucket objects require both `s3:GetObject` and `kms:Decrypt`. The KMS key policy must also allow the role. Two separate policy documents, two separate denial points.
|
||||
- **No resource policy for new trigger.** If you wire up EventBridge manually (not via the console), the trigger silently fails because there's no resource policy entry granting EventBridge `lambda:InvokeFunction`.
|
||||
|
||||
## Diagnosing permission errors
|
||||
|
||||
CloudTrail is the ground truth. Filter by `errorCode: "AccessDenied"` and `userIdentity.arn` matching the execution role ARN. The event tells you exactly which action on which resource was denied. CloudWatch will show the error in the Lambda log if you let the exception propagate, but CloudTrail shows it even when the call is made from a library that swallows the error.
|
||||
54
docs/lambdas-md/lambda-08-packaging.md
Normal file
54
docs/lambdas-md/lambda-08-packaging.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Packaging
|
||||
|
||||
> Zip vs layers vs container images. arm64 vs x86_64. Native wheels.
|
||||
|
||||
## Three deployment formats
|
||||
|
||||
| Format | Size limit | Best for | Caveats |
|
||||
|--------|-----------|----------|---------|
|
||||
| **Zip (direct)** | 50 MB upload / 250 MB unzipped | Most Python/Node functions with pure-Python or pre-built wheels | Must match Lambda's architecture; no custom runtime |
|
||||
| **Zip via S3** | 250 MB unzipped | Same as above but when zip exceeds 50 MB | S3 bucket must be in the same region |
|
||||
| **Layers** | 250 MB total (function + all layers) | Shared dependencies across functions (e.g. a company-wide logging layer) | Max 5 layers per function; later layers overwrite earlier ones |
|
||||
| **Container image** | 10 GB | ML models, native binary deps, custom runtimes | Slower first cold start (image pull); larger attack surface |
|
||||
|
||||
## Layers in practice
|
||||
|
||||
A layer is a zip file that Lambda extracts into `/opt` before running your function. Your code in `/var/task` can import from `/opt/python` (for Python) without any path manipulation. Use cases:
|
||||
|
||||
- Shared internal libraries deployed independently of business logic
|
||||
- Large dependencies that change rarely (numpy, pandas) — cache them in a layer so deployments of the business logic are fast
|
||||
- AWS-provided layers: Lambda Insights extension, X-Ray SDK
|
||||
|
||||
Layers count toward the 250 MB unzipped limit. If you have 5 layers at 40 MB each and your function zip is 50 MB, you're at 250 MB — no room left.
|
||||
|
||||
## Container images
|
||||
|
||||
Container images must be based on AWS-provided base images (`public.ecr.aws/lambda/python:3.13`) or implement the Lambda Runtime Interface. They must be stored in ECR (Elastic Container Registry) in the same region. The Lambda service caches images on the underlying host after the first pull, so subsequent cold starts on the same host are fast — but the very first invocation after a new image is deployed can be slow for large images.
|
||||
|
||||
Container images bypass the 250 MB unzipped limit, which is why they're the standard choice for Python ML workloads that bundle PyTorch or TensorFlow.
|
||||
|
||||
## arm64 vs x86_64
|
||||
|
||||
Graviton2-based arm64 is ~20% cheaper per GB-second than x86_64 and typically faster at compute-heavy work. The decision tree:
|
||||
|
||||
1. Check all your dependencies for arm64 wheels: `pip download --platform manylinux2014_aarch64 --only-binary :all: -r requirements.txt`. If any fail, you either build from source (needs Dockerfile) or stay on x86.
|
||||
2. For pure-Python deps and most modern packages, arm64 works out of the box.
|
||||
3. Native extensions (cryptography, numpy, psycopg2) have arm64 wheels on PyPI since ~2022. Check the exact version you need.
|
||||
|
||||
## Building for Lambda (the common foot-gun)
|
||||
|
||||
Lambda runs on Amazon Linux 2023. `pip install` on macOS produces wheels compiled for macOS, which will segfault or import-error on Lambda. The correct approach:
|
||||
|
||||
```bash
|
||||
# build inside the Lambda runtime image
|
||||
docker run --rm \
|
||||
-v "$PWD":/var/task \
|
||||
public.ecr.aws/lambda/python:3.13 \
|
||||
pip install -r requirements.txt -t python/
|
||||
|
||||
zip -r layer.zip python/
|
||||
```
|
||||
|
||||
This is also where architecture matters: use the `:3.13-arm64` tag when building for arm64.
|
||||
|
||||
> **This project** uses a zip deployment. `aioboto3` and `aiofiles` are pure-Python and have no native extensions, so they build cleanly on any architecture. The Makefile's `install` target creates a local `.venv` for development; a real CI pipeline would build the deployment zip inside the Lambda image.
|
||||
46
docs/lambdas-md/lambda-09-vpc-networking.md
Normal file
46
docs/lambdas-md/lambda-09-vpc-networking.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# VPC & Networking
|
||||
|
||||
> When to put Lambda in a VPC (rarely). ENI cold start cost. NAT money pit.
|
||||
|
||||
## Default: no VPC
|
||||
|
||||
By default, Lambda runs in an AWS-managed network with internet access. It can reach S3, DynamoDB, SQS, and other AWS services via their public endpoints. **Do not put Lambda in a VPC unless you have a specific reason.** Most applications don't need it.
|
||||
|
||||
## When you actually need VPC
|
||||
|
||||
- Connecting to RDS or Aurora (which live in a private subnet)
|
||||
- ElastiCache (Redis/Memcached) — VPC-only by design
|
||||
- Private REST APIs or internal services on private subnets
|
||||
- Compliance requirements mandating network isolation
|
||||
|
||||
S3, DynamoDB, SQS, SNS, and most AWS managed services do **not** require VPC placement — they're public services with public endpoints.
|
||||
|
||||
## ENI attachment and cold start
|
||||
|
||||
When Lambda is VPC-attached, each execution environment gets an Elastic Network Interface (ENI) in your VPC. Pre-2019, ENIs were allocated per cold start, adding 10–30 s to init. AWS fixed this in 2019 with hyperplane ENIs shared across environments — today the VPC cold start penalty is ~100–500 ms on the first cold start of a new deployment, then negligible. It's no longer the dealbreaker it used to be, but it's not zero.
|
||||
|
||||
## Subnet and AZ placement
|
||||
|
||||
Specify at least two subnets in different AZs for availability. Lambda will distribute environments across AZs. If a subnet runs out of available ENI slots (IP exhaustion), Lambda scaling fails — size subnets with this in mind. /24 (254 IPs) is often too small for high-concurrency functions.
|
||||
|
||||
## The NAT money pit
|
||||
|
||||
VPC Lambda can't reach the internet by default. If your function needs to call an external API or reach an AWS service without a VPC endpoint, you need a NAT gateway in a public subnet. NAT gateways cost:
|
||||
|
||||
- **$0.045/hour** (~$32/month) just to exist, per AZ
|
||||
- **$0.045/GB** of data processed
|
||||
|
||||
A function that sends 100 GB/month through NAT costs $4.50 in data alone, on top of the always-on hourly charge. Two AZs for HA = ~$64/month base cost before a single byte of traffic. This is frequently the largest unexpected cost in VPC Lambda setups.
|
||||
|
||||
## VPC endpoints: the free alternative
|
||||
|
||||
For AWS services, VPC endpoints bypass NAT and the public internet entirely. Two types:
|
||||
|
||||
- **Gateway endpoints** — S3 and DynamoDB only. Free. Route table entries. No data charge.
|
||||
- **Interface endpoints (PrivateLink)** — any AWS service. $0.01/AZ/hr + $0.01/GB. Expensive for high throughput but often cheaper than NAT for AWS-service-heavy workloads.
|
||||
|
||||
For a VPC Lambda that only talks to S3 and DynamoDB: create gateway endpoints for both → no NAT needed → near-zero networking cost.
|
||||
|
||||
## Security groups
|
||||
|
||||
VPC Lambda gets a security group. Outbound rules control where it can connect. The security group of RDS/ElastiCache must allow inbound from the Lambda security group. A common pattern is to create a dedicated Lambda SG and reference it in the database SG's inbound rules — this avoids IP-range rules that break when Lambda ENIs change.
|
||||
93
docs/lambdas-md/lambda-10-observability.md
Normal file
93
docs/lambdas-md/lambda-10-observability.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Observability
|
||||
|
||||
> CloudWatch logs, structured JSON, X-Ray, Lambda Insights, EMF. Brief Prometheus/Grafana orientation.
|
||||
|
||||
## CloudWatch Logs — what you get for free
|
||||
|
||||
Every Lambda function automatically writes to a CloudWatch Log Group named `/aws/lambda/<function-name>`. Each execution environment gets its own Log Stream. Lambda writes two special lines automatically:
|
||||
|
||||
```
|
||||
START RequestId: abc-123 Version: $LATEST
|
||||
END RequestId: abc-123
|
||||
REPORT RequestId: abc-123 Duration: 312.45 ms Billed Duration: 313 ms
|
||||
Memory Size: 256 MB Max Memory Used: 89 MB
|
||||
Init Duration: 423.12 ms # only on cold starts
|
||||
```
|
||||
|
||||
The REPORT line is your free performance telemetry. `Init Duration` appears only on cold invocations. `Max Memory Used` helps right-size memory configuration.
|
||||
|
||||
**Retention:** Default is "Never Expire." Set it explicitly — 7, 14, or 30 days covers most needs. Every MB of retained logs costs money.
|
||||
|
||||
## Structured logging
|
||||
|
||||
Emit JSON instead of plain strings. CloudWatch Logs Insights can filter and aggregate JSON fields efficiently; plain strings require regex and are slow. Example:
|
||||
|
||||
```python
|
||||
import json, logging
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def handler(event, context):
|
||||
logger.info(json.dumps({
|
||||
"event": "pdf_scan_start",
|
||||
"bucket": BUCKET,
|
||||
"prefix": PREFIX,
|
||||
"request_id": context.aws_request_id,
|
||||
}))
|
||||
```
|
||||
|
||||
With this, Logs Insights can run: `filter event = "pdf_scan_start" | stats count() by bin(5m)` in seconds.
|
||||
|
||||
## X-Ray tracing
|
||||
|
||||
X-Ray gives you request traces across services — how long the Lambda itself ran vs how long S3 calls took. Three things must all be true:
|
||||
|
||||
1. **Tracing enabled on the function** — console toggle or `TracingConfig: Active` in SAM/CDK
|
||||
2. **X-Ray SDK instrumented in your code** — `from aws_xray_sdk.core import patch_all; patch_all()` wraps boto3 calls automatically
|
||||
3. **IAM permission** — execution role needs `xray:PutTraceSegments` and `xray:PutTelemetryRecords`
|
||||
|
||||
Without all three, traces are either absent or incomplete. People flip one and conclude X-Ray is broken.
|
||||
|
||||
## Lambda Insights
|
||||
|
||||
Lambda Insights is a CloudWatch feature (not a separate service) that surfaces system-level metrics: CPU usage, memory utilisation, network I/O, disk I/O — things the REPORT line doesn't include. To enable it:
|
||||
|
||||
- Add the Lambda Insights extension layer (`arn:aws:lambda:<region>:580247275435:layer:LambdaInsightsExtension:38`)
|
||||
- Add `cloudwatch:PutMetricData` to the execution role
|
||||
|
||||
It's useful when you suspect memory or CPU contention but the REPORT line's "Max Memory Used" isn't granular enough.
|
||||
|
||||
## EMF — Embedded Metrics Format
|
||||
|
||||
EMF lets you emit custom CloudWatch metrics by writing structured JSON to stdout. No `PutMetricData` API call needed — the Lambda runtime parses the log line and publishes the metric asynchronously. This is far more efficient than calling CloudWatch from inside the handler (which adds latency + cost per invocation).
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
def emit_metric(name, value, unit="Count", **dims):
|
||||
print(json.dumps({
|
||||
"_aws": {
|
||||
"Timestamp": int(time.time() * 1000),
|
||||
"CloudWatchMetrics": [{
|
||||
"Namespace": "MyApp",
|
||||
"Dimensions": [list(dims.keys())],
|
||||
"Metrics": [{"Name": name, "Unit": unit}]
|
||||
}]
|
||||
},
|
||||
name: value,
|
||||
**dims,
|
||||
}))
|
||||
|
||||
# usage
|
||||
emit_metric("PDFsProcessed", count, Unit="Count", Function="pdf-scanner")
|
||||
```
|
||||
|
||||
## Prometheus & Grafana (brief)
|
||||
|
||||
Prometheus uses a **pull model** — it scrapes HTTP endpoints. Lambda functions are ephemeral and have no persistent HTTP endpoint, so Prometheus can't scrape them directly. Approaches:
|
||||
|
||||
- **EMF → CloudWatch → Grafana CloudWatch plugin** — easiest; Grafana queries CW as a data source
|
||||
- **Amazon Managed Prometheus (AMP) + remote_write** — Lambda pushes metrics to AMP via the Prometheus remote write API; Grafana (or Amazon Managed Grafana) reads from AMP. Requires the `prometheus_client` library and SIGV4 signing on the remote_write request.
|
||||
- **Statsd/push gateway** — Lambda pushes to a persistent push gateway; Prometheus scrapes the gateway. More infra to manage, stale metric risk if the push gateway isn't flushed between invocations.
|
||||
|
||||
For Lambda-centric dashboards, the CloudWatch → Grafana path is usually the simplest to operate.
|
||||
73
docs/lambdas-md/lambda-11-async-errors.md
Normal file
73
docs/lambdas-md/lambda-11-async-errors.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Async & Errors
|
||||
|
||||
> Sync vs async invoke. Retries, DLQ, destinations, idempotency, partial-batch failures.
|
||||
|
||||
## Sync vs async invocation
|
||||
|
||||
| | Synchronous (RequestResponse) | Asynchronous (Event) |
|
||||
|--|-------------------------------|----------------------|
|
||||
| **Caller blocks?** | Yes — waits for result | No — gets 202 immediately |
|
||||
| **Response visible to caller?** | Yes | No |
|
||||
| **Retries on error** | None (caller's responsibility) | 2 retries = 3 total attempts |
|
||||
| **Retry backoff** | — | ~1 min then ~2 min |
|
||||
| **Event age limit** | — | 6 hours |
|
||||
| **Max event size** | 6 MB | 256 KB |
|
||||
|
||||
## Async retry flow
|
||||
|
||||
When Lambda invokes asynchronously and the function throws an unhandled exception (or is throttled), Lambda retries automatically — twice, with exponential backoff starting at ~1 minute. If all three attempts fail, or if the event ages past 6 hours, Lambda sends the event to the configured failure destination or DLQ. If neither is configured, the event is silently dropped.
|
||||
|
||||
## DLQ vs Destinations
|
||||
|
||||
These are two different mechanisms that overlap in purpose but have different capabilities:
|
||||
|
||||
| | Dead-Letter Queue (DLQ) | Event Destinations |
|
||||
|--|-------------------------|---------------------|
|
||||
| **Introduced** | 2016 (legacy) | 2019 (preferred) |
|
||||
| **Triggers on** | Failure only | Success or failure (separate configs) |
|
||||
| **Payload** | The original event only | Original event + result/error + metadata |
|
||||
| **Targets** | SQS or SNS | SQS, SNS, Lambda, EventBridge |
|
||||
|
||||
Use Destinations for new code. DLQ remains useful when the downstream consumer must be SQS and you don't need success notifications.
|
||||
|
||||
## Idempotency
|
||||
|
||||
Because async invocations retry and most event sources are at-least-once, your handler will occasionally execute more than once for the same logical event. Design handlers to be idempotent — the same input produces the same outcome regardless of how many times it runs.
|
||||
|
||||
Standard pattern: use a unique key from the event (S3 ETag + key, SQS MessageId, EventBridge detail.id) as a deduplication key. On first execution, write the key + result to DynamoDB with a TTL. On retry, check DynamoDB first — if already processed, return the cached result without re-running the work.
|
||||
|
||||
```python
|
||||
# pseudo-code
|
||||
dedup_key = event["Records"][0]["messageId"]
|
||||
existing = table.get_item(Key={"id": dedup_key})
|
||||
if existing.get("Item"):
|
||||
return existing["Item"]["result"]
|
||||
|
||||
result = do_the_work(event)
|
||||
table.put_item(Item={"id": dedup_key, "result": result, "ttl": now + 86400})
|
||||
return result
|
||||
```
|
||||
|
||||
AWS PowerTools for Lambda (Python) has a built-in `@idempotent` decorator that implements this pattern with DynamoDB.
|
||||
|
||||
## Partial batch failures (SQS / Kinesis / DynamoDB Streams)
|
||||
|
||||
When Lambda processes a batch of records and one record fails, the default behaviour differs by source:
|
||||
|
||||
- **SQS (default)**: if the handler raises an exception, the entire batch is retried. One bad message blocks all others and can cause infinite retry loops.
|
||||
- **With `ReportBatchItemFailures` enabled**: return a `batchItemFailures` list containing only the failed message IDs. Lambda re-queues only those; successful messages are deleted.
|
||||
|
||||
```python
|
||||
def handler(event, context):
|
||||
failures = []
|
||||
for record in event["Records"]:
|
||||
try:
|
||||
process(record)
|
||||
except Exception:
|
||||
failures.append({"itemIdentifier": record["messageId"]})
|
||||
return {"batchItemFailures": failures}
|
||||
```
|
||||
|
||||
Enable `ReportBatchItemFailures` in the ESM configuration and always implement partial-batch failure reporting for SQS and Kinesis handlers. A single poison-pill record can otherwise block an entire shard or queue indefinitely.
|
||||
|
||||
> ⚠️ **The idempotency–partial-batch intersection:** with partial failures, successful records in the batch are deleted from SQS, but if your function crashes before returning the failure list, the entire batch including the successes gets retried. Idempotency guards must still cover every record, not just the ones in `batchItemFailures`.
|
||||
65
docs/lambdas-md/lambda-12-step-functions.md
Normal file
65
docs/lambdas-md/lambda-12-step-functions.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Step Functions
|
||||
|
||||
> When Lambda alone isn't enough. Standard vs Express. Map state for fan-out. Comparison with Airflow.
|
||||
|
||||
## When Lambda alone isn't enough
|
||||
|
||||
A single Lambda function works well for one discrete task. Problems start when you need to chain multiple tasks, retry selectively, wait on human approval, or fan out across thousands of items. Doing this with Lambda alone means writing orchestration logic inside your functions — tracking state, implementing retry delays, deciding what "done" means. Step Functions externalises that orchestration into a state machine where every state transition is durable, auditable, and resumable.
|
||||
|
||||
Reach for Step Functions when you need: sequential steps with state passing, conditional branching, parallel fan-out with join, wait states longer than 15 minutes, or retry-with-exponential-backoff built in.
|
||||
|
||||
## Standard vs Express workflows
|
||||
|
||||
| | Standard | Express |
|
||||
|--|----------|---------|
|
||||
| **Max duration** | 1 year | 5 minutes |
|
||||
| **Execution semantics** | Exactly-once per state | At-least-once |
|
||||
| **Execution history** | Full audit trail in AWS console | CloudWatch Logs only |
|
||||
| **Pricing** | $0.025 per 1 000 state transitions | $0.00001 per state transition + duration |
|
||||
| **Use for** | Long-running business workflows, human approvals, compliance audit trails | High-volume, short-duration event processing (IoT, streaming) |
|
||||
|
||||
For most application orchestration, Standard is the right choice — the exactly-once semantic matters when steps have side effects (charging a card, sending an email). Express is for high-throughput pipelines where at-least-once is acceptable and cost per transition is a concern.
|
||||
|
||||
## Map state for fan-out
|
||||
|
||||
The Map state runs the same workflow branch for every item in an array, in parallel. This is the core fan-out primitive. For this project's use case, a Step Functions version could fan out across S3 prefixes — run one Lambda per prefix, collect results in a fan-in step:
|
||||
|
||||
```json
|
||||
{
|
||||
"Type": "Map",
|
||||
"ItemsPath": "$.prefixes",
|
||||
"MaxConcurrency": 10,
|
||||
"Iterator": {
|
||||
"StartAt": "ScanPrefix",
|
||||
"States": {
|
||||
"ScanPrefix": {
|
||||
"Type": "Task",
|
||||
"Resource": "arn:aws:lambda:...:function:pdf-scanner",
|
||||
"End": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`MaxConcurrency: 0` means unlimited — bounded only by the Lambda concurrency pool. Set an explicit cap to avoid saturating the account concurrency quota.
|
||||
|
||||
## Other useful states
|
||||
|
||||
- **Wait** — pause for a duration or until a timestamp. The only way to implement delays longer than 15 minutes without polling.
|
||||
- **Choice** — conditional branching on input values. Replaces `if/else` logic that would otherwise live inside a Lambda.
|
||||
- **Parallel** — run multiple independent branches simultaneously and join their results.
|
||||
- **Task (SDK integrations)** — Step Functions can call DynamoDB, SQS, ECS, Glue, etc. directly without a Lambda wrapper, reducing cost and latency for simple operations.
|
||||
|
||||
## Step Functions vs Airflow
|
||||
|
||||
| | Step Functions | Apache Airflow (MWAA) |
|
||||
|--|----------------|------------------------|
|
||||
| **DAG definition** | JSON/YAML state machine (ASL) | Python code (DAG files) |
|
||||
| **Scheduling** | Event-driven / on-demand; cron via EventBridge | Built-in rich scheduler (cron, data-interval-aware) |
|
||||
| **Backfill** | Manual / custom | First-class, built-in |
|
||||
| **Operators** | AWS services + Lambda (AWS ecosystem only) | 600+ providers: Spark, BigQuery, dbt, Kubernetes… |
|
||||
| **Infrastructure** | Serverless — zero infra | Managed Airflow (MWAA) starts at ~$400/month |
|
||||
| **Debugging** | Console execution graph; CloudWatch for logs | Airflow UI with task logs, Gantt charts, retries |
|
||||
|
||||
Step Functions is the right choice when your workflow is AWS-native, event-driven, and you want zero infrastructure. Airflow is the right choice when you need complex scheduling, data-interval backfill, cross-cloud operators, or a data-engineering team that already knows Python DAGs.
|
||||
42
docs/lambdas-md/lambda-13-cost.md
Normal file
42
docs/lambdas-md/lambda-13-cost.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Cost
|
||||
|
||||
> Pricing model, memory/cost trade-off, x86 vs arm64, free tier, common surprises.
|
||||
|
||||
## The pricing formula
|
||||
|
||||
Lambda billing has two components, both permanent free tiers included:
|
||||
|
||||
| Component | x86_64 | arm64 | Free tier (permanent) |
|
||||
|-----------|--------|-------|------------------------|
|
||||
| **Requests** | $0.20 / 1M | $0.20 / 1M | 1M / month |
|
||||
| **Duration** | $0.0000166667 / GB-s | $0.0000133334 / GB-s | 400 000 GB-s / month |
|
||||
|
||||
GB-seconds = memory configured (GB) × duration (seconds). A 512 MB function running for 300 ms = 0.5 × 0.3 = 0.15 GB-s. At 1 million invocations, that's 150 000 GB-s — well inside the free tier.
|
||||
|
||||
Duration is billed in **1 ms increments**. The old 100 ms minimum is gone (removed in 2020).
|
||||
|
||||
## Memory vs cost: more can be cheaper
|
||||
|
||||
CPU scales linearly with memory. A function configured at 1 769 MB gets a full vCPU; below that it's a fraction. Doubling memory often more than halves duration for CPU-bound work, which means the total GB-s cost stays the same or decreases — while latency drops.
|
||||
|
||||
**AWS Lambda Power Tuning** is a Step Functions state machine that automatically benchmarks your function at multiple memory sizes and produces a cost/performance curve. Run it before guessing at the right memory setting. The optimal point is almost never the default 128 MB.
|
||||
|
||||
## arm64 saves ~20%
|
||||
|
||||
arm64 duration pricing is 20% cheaper than x86. Same request price. If your function is compute-bound (not I/O-bound sleeping on S3 calls), arm64 also runs faster, compounding the saving. For I/O-bound functions (like `lambda_function.py`, which spends most of its time waiting on S3), the duration difference is smaller but the 20% price reduction still applies.
|
||||
|
||||
## Provisioned Concurrency billing
|
||||
|
||||
PC is billed separately: $0.0000097222 per GB-s of provisioned time (x86) — even when idle. If you have 10 × 512 MB environments provisioned for 24 hours: 10 × 0.5 GB × 86 400 s = 432 000 GB-s/day = ~$4.20/day = ~$126/month just for the warm slots, before counting actual invocation cost on top. PC is for latency, not cost — it always increases your bill.
|
||||
|
||||
## Hidden costs (the real bill)
|
||||
|
||||
- **NAT Gateway** — $0.045/hr per AZ (~$32/month) + $0.045/GB data. Often the largest line item for VPC Lambda.
|
||||
- **API Gateway** — REST API: $3.50/1M calls. HTTP API: $1/1M. Can dwarf Lambda cost at high RPS.
|
||||
- **CloudWatch Logs** — $0.50/GB ingestion + $0.03/GB storage/month. Verbose Lambda logs accumulate fast; set retention.
|
||||
- **Lambda Insights** — additional CW Logs + custom metrics charges.
|
||||
- **X-Ray** — $5/million traces (after free 100K/month).
|
||||
- **Data transfer** — traffic leaving a region or going through a NAT has per-GB charges.
|
||||
- **S3 API calls** — LIST and GET requests are billed per 1 000. A function that does 10 000 LIST calls/invocation at 1M invocations = 10B API calls = real money.
|
||||
|
||||
> ✅ **For this project's function:** at 1 000 invocations/day with 500 ms average duration and 256 MB memory, cost is ~$0.002/day — essentially free. Lambda's economics only require attention above ~100K invocations/day with non-trivial memory or duration.
|
||||
74
docs/lambdas-md/lambda-14-local-dev.md
Normal file
74
docs/lambdas-md/lambda-14-local-dev.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Local Dev
|
||||
|
||||
> SAM CLI, Lambda RIE, LocalStack, MinIO — when to reach for which.
|
||||
|
||||
## The local dev problem
|
||||
|
||||
Lambda has no local runtime by default. Your only loop without tooling is: zip, upload, invoke, read CloudWatch logs, repeat — minutes per cycle. The tools below collapse that to seconds, with different trade-offs between fidelity, setup cost, and scope.
|
||||
|
||||
## SAM CLI
|
||||
|
||||
**What it is:** AWS's official local Lambda emulator. Wraps Docker to run your function inside a container that matches the Lambda runtime environment exactly. Also emulates API Gateway.
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
sam local invoke -e event.json # invoke once
|
||||
sam local start-api # spin up local HTTP API gateway
|
||||
sam local invoke --debug-port 5858 # attach debugger
|
||||
```
|
||||
|
||||
**Fidelity:** high — same Amazon Linux image, same runtime, same filesystem layout. Catches architecture issues (x86 wheel on arm64) that a plain venv misses.
|
||||
|
||||
**Downsides:** requires Docker, slow to start (pulls image on first run), no MinIO/SQS/DynamoDB emulation built in. You wire those up separately.
|
||||
|
||||
## Lambda Runtime Interface Emulator (RIE)
|
||||
|
||||
A lightweight binary embedded in all AWS-provided Lambda base images. When you run the image locally, RIE exposes a local HTTP endpoint that accepts invocations in the Lambda API format. You don't need SAM CLI — just Docker:
|
||||
|
||||
```bash
|
||||
docker build -t my-fn .
|
||||
docker run -p 9000:8080 my-fn
|
||||
curl -XPOST http://localhost:9000/2015-03-31/functions/function/invocations \
|
||||
-d '{"key": "value"}'
|
||||
```
|
||||
|
||||
Use RIE when you're building container-image Lambdas and want to test them without SAM overhead.
|
||||
|
||||
## LocalStack
|
||||
|
||||
A full AWS mock that emulates Lambda, S3, SQS, DynamoDB, API Gateway, and dozens more services in a single container. Community edition is free; Pro ($35/month) adds more services and persistent state.
|
||||
|
||||
**When to use:** integration tests that span multiple AWS services (e.g. an EventBridge rule that triggers a Lambda that writes to DynamoDB). Without LocalStack you'd need a real AWS account for these tests.
|
||||
|
||||
**When to avoid:** if you only need one service (just S3 → use MinIO; just Lambda → use SAM/RIE). LocalStack's Lambda emulation has occasional edge-case differences from the real runtime.
|
||||
|
||||
```bash
|
||||
docker run --rm -p 4566:4566 localstack/localstack
|
||||
AWS_DEFAULT_REGION=us-east-1 \
|
||||
AWS_ACCESS_KEY_ID=test \
|
||||
AWS_SECRET_ACCESS_KEY=test \
|
||||
aws --endpoint-url=http://localhost:4566 s3 ls
|
||||
```
|
||||
|
||||
## MinIO (this project)
|
||||
|
||||
MinIO is an S3-compatible object store that runs locally in Docker. It implements the S3 API precisely enough that `boto3`/`aioboto3` needs only an `endpoint_url` override to work against it. It is **not** a Lambda emulator — it replaces S3 only.
|
||||
|
||||
```bash
|
||||
make up # starts MinIO on :9000 (API) and :9001 (console)
|
||||
SOURCE_DIR=~/pdfs make seed # uploads PDFs to MinIO
|
||||
make invoke # runs lambda_function.py against MinIO via invoke.py
|
||||
```
|
||||
|
||||
This is the lightest possible local setup: no Docker-in-Docker, no SAM overhead, minimal latency. The function handler runs in your local Python process against a real S3-compatible store. Differences from real Lambda (no execution environment lifecycle, no /tmp isolation between runs) are acceptable for the development loop but not for environment-fidelity tests.
|
||||
|
||||
## Decision matrix
|
||||
|
||||
| Need | Reach for |
|
||||
|------|-----------|
|
||||
| Fast iteration on handler logic | MinIO + `python invoke.py` (this project's setup) |
|
||||
| Emulate Lambda runtime + API Gateway locally | SAM CLI |
|
||||
| Test a container-image Lambda | Lambda RIE via Docker |
|
||||
| Integration test across multiple AWS services | LocalStack |
|
||||
| Full-fidelity staging before prod | Real AWS account, separate environment |
|
||||
75
docs/lambdas-md/lambda-15-cicd.md
Normal file
75
docs/lambdas-md/lambda-15-cicd.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# CI/CD
|
||||
|
||||
> Aliases, versions, traffic shifting, blue/green. Plain CLI → SAM → CDK → Terraform.
|
||||
|
||||
## Versions and aliases
|
||||
|
||||
**Versions** are immutable snapshots of a function's code and configuration. When you publish a version (`aws lambda publish-version`), AWS creates an immutable ARN like `arn:…:function:my-fn:7`. `$LATEST` is the only mutable version — always reflects the most recent code upload.
|
||||
|
||||
**Aliases** are named pointers to a version. `prod` might point to version 7; `staging` might point to version 8. Event source mappings, API Gateway integrations, and Step Functions tasks should target aliases, not version ARNs — this decouples deployment (publishing a new version) from promotion (updating the alias).
|
||||
|
||||
## Traffic shifting (blue/green)
|
||||
|
||||
An alias can split traffic across two versions with weighted routing:
|
||||
|
||||
```bash
|
||||
aws lambda update-alias \
|
||||
--function-name my-fn \
|
||||
--name prod \
|
||||
--function-version 8 \
|
||||
--routing-config AdditionalVersionWeights={"7"=0.9}
|
||||
# result: 10% of prod traffic goes to v8, 90% still to v7
|
||||
```
|
||||
|
||||
Start at 10% canary, watch error rates in CloudWatch, shift to 50%, then 100%. Rollback is instant: point the alias back to the stable version. No instance drain, no connection draining — Lambda is stateless, cutover is atomic.
|
||||
|
||||
## CodeDeploy integration
|
||||
|
||||
SAM and CDK can wire up CodeDeploy for automatic traffic shifting with automatic rollback on CloudWatch alarms. You declare the deployment preference in the template:
|
||||
|
||||
```yaml
|
||||
# SAM template.yaml
|
||||
DeploymentPreference:
|
||||
Type: Canary10Percent5Minutes # 10% for 5 min, then 100%
|
||||
Alarms:
|
||||
- !Ref ErrorRateAlarm # rolls back if alarm triggers
|
||||
```
|
||||
|
||||
CodeDeploy manages the alias weight changes and calls the rollback if the alarm fires — fully automated blue/green without manual traffic management.
|
||||
|
||||
## Deployment tooling progression
|
||||
|
||||
| Tool | Good for | Caveats |
|
||||
|------|----------|---------|
|
||||
| **AWS CLI / SDK** | One-off deployments, scripting, deep control | Verbose; no state management; drift-prone at scale |
|
||||
| **SAM (CloudFormation extension)** | Lambda-first projects; built-in local testing; CodeDeploy integration | CloudFormation speed; YAML verbosity; AWS-only |
|
||||
| **CDK** | Complex infra in TypeScript/Python; reusable constructs; type safety | Still compiles to CloudFormation; learning curve; bootstrapping required |
|
||||
| **Terraform (AWS provider)** | Multi-cloud orgs; large existing Terraform estate; strong community modules | No built-in Lambda local testing; plan/apply cycle slower than SAM deploy |
|
||||
| **Serverless Framework** | Multi-cloud serverless; plugin ecosystem | V3 → V4 became paid for teams; community plugins vary in quality |
|
||||
|
||||
## CI pipeline skeleton
|
||||
|
||||
```yaml
|
||||
# GitHub Actions example
|
||||
jobs:
|
||||
deploy:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build zip
|
||||
run: |
|
||||
docker run --rm -v $PWD:/var/task \
|
||||
public.ecr.aws/lambda/python:3.13 \
|
||||
pip install -r requirements.txt -t package/
|
||||
cd package && zip -r ../function.zip . && cd ..
|
||||
zip function.zip lambda_function.py
|
||||
- name: Deploy
|
||||
run: |
|
||||
aws lambda update-function-code \
|
||||
--function-name my-fn --zip-file fileb://function.zip
|
||||
aws lambda wait function-updated --function-name my-fn
|
||||
aws lambda publish-version --function-name my-fn
|
||||
aws lambda update-alias --function-name my-fn \
|
||||
--name prod --function-version $VERSION
|
||||
```
|
||||
|
||||
The `wait function-updated` call is important — `update-function-code` is asynchronous and `publish-version` must wait for it to complete.
|
||||
57
docs/lambdas-md/lambda-16-pitfalls.md
Normal file
57
docs/lambdas-md/lambda-16-pitfalls.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Pitfalls — The Must-Knows
|
||||
|
||||
> The list to skim before the next interview or design review. Each item has bitten someone in production.
|
||||
|
||||
## Execution model
|
||||
|
||||
1. **Module-level state leaks across invocations.** A list you append to in the handler grows forever on warm calls. A counter you increment is wrong by the second request. If it's mutable and lives at module scope, treat it as either a deliberate cache or a bug.
|
||||
2. **Handler globals are shared by every invocation on that env, but not across envs.** "I cached the result" works locally; in production half your traffic gets the cached value, the other half doesn't, depending on which warm container they hit. Externalise (Redis, DynamoDB) or accept the variance.
|
||||
3. **/tmp is per-environment, not per-invocation.** If you write `/tmp/output.json` with a fixed name, the next warm invocation finds yesterday's file. Always use a per-invocation suffix (UUID, request ID).
|
||||
4. **Init phase has a hard 10 s cap.** If you import TensorFlow, hydrate a 500 MB model, or do a network call at module scope, you can blow this budget on cold start. Defer expensive work until first handler call (lazy init), or move it to a layer that ships pre-warmed.
|
||||
5. **Async `asyncio.run` in a sync handler creates a fresh event loop per invocation.** Acceptable, but means async clients can't be shared across invocations the way sync boto3 clients can. Profile before assuming async is faster.
|
||||
|
||||
## Payload & size limits
|
||||
|
||||
6. **6 MB sync response cap is silent.** Returning a JSON list of 50 000 items "works" in the function but the API GW caller gets 413. The fix in `lambda_function.py` — return a presigned URL to a manifest file rather than the full list — is the standard pattern.
|
||||
7. **API Gateway caps integration time at 29 s.** Doesn't matter if your Lambda timeout is 15 minutes. For longer work, return a job ID and poll, or use Function URLs (15 min) with response streaming.
|
||||
8. **Environment variables max 4 KB total.** Big secrets (RSA keys, JSON config blobs) blow this. Parameter Store / Secrets Manager and read on init.
|
||||
|
||||
## Concurrency & throttling
|
||||
|
||||
9. **Default account concurrency is 1 000 per region.** Most teams hit this before they realise. Sets a hard ceiling on RPS — at 100 ms latency, that's 10 000 RPS account-wide; at 1 s, 1 000 RPS.
|
||||
10. **Reserved concurrency = 0 disables the function.** Looks weird, used as a circuit breaker.
|
||||
11. **Provisioned concurrency double-bills.** You pay for the warm slots *and* for invocations against them. Worth it for latency-sensitive paths; wasteful for batch.
|
||||
12. **Burst limit is regional and finite.** A traffic spike from 0 to 5 000 RPS will throttle until AWS scales up at +500 envs/min. Provisioned concurrency or pre-warming is the fix.
|
||||
|
||||
## Triggers, retries, idempotency
|
||||
|
||||
13. **Async invocation retries 2 times by default.** Total 3 attempts. If your handler isn't idempotent, you can charge a card three times.
|
||||
14. **S3, SNS, EventBridge invoke async — at-least-once.** Plan for duplicates. SQS standard is also at-least-once. SQS FIFO and Kinesis are exactly-once-ish per shard but with their own quirks.
|
||||
15. **SQS visibility timeout must be ≥ 6× function timeout.** Otherwise the message comes back while you're still processing it, and you do the work twice (or more).
|
||||
16. **Partial batch failures need explicit signalling.** Returning `batchItemFailures` for SQS/Kinesis tells AWS which records to retry; otherwise the entire batch retries or none does.
|
||||
17. **API Gateway error responses are JSON-shaped if you don't say otherwise.** Throw an unhandled exception and the client sees `{"errorMessage": "...", "errorType": "..."}` with status 502. Map errors yourself.
|
||||
|
||||
## Networking, IAM, observability
|
||||
|
||||
18. **Putting Lambda in a VPC adds an ENI cold-start penalty** (improved a lot in 2019, but still real for first invocation). Only do it if you genuinely need private-subnet resources. Outbound internet from VPC Lambda needs NAT, which costs money 24/7.
|
||||
19. **S3 access from a VPC Lambda needs a VPC gateway endpoint or NAT.** Without one, your S3 calls hang and time out — looks like a code bug, isn't.
|
||||
20. **CloudWatch log groups default to "Never expire" retention.** Verbose Lambdas can rack up real cost in CW Logs alone — set retention (7/14/30 days) on every log group you create.
|
||||
21. **Lambda execution role is implicit on every action.** Forgetting `s3:GetObject` or `kms:Decrypt` on the bucket's CMK is the most common "but it works locally" failure. CloudTrail tells you what was denied.
|
||||
22. **Resource policy vs execution role are different layers.** Resource policy says "who can *invoke* this Lambda"; execution role says "what this Lambda can *do*". Both must allow.
|
||||
23. **X-Ray needs an SDK call *and* tracing enabled on the function *and* IAM permission.** Three switches. People flip one and conclude X-Ray is broken.
|
||||
|
||||
## Deployment, dependencies, runtimes
|
||||
|
||||
24. **The boto3 in the Python runtime lags pip.** If you need a recent API (e.g. new S3 features), bundle current boto3 in your zip. The runtime version is "good enough" for stable APIs, "sometimes wrong" for fresh ones.
|
||||
25. **Native wheels must match Lambda's runtime architecture.** `pip install` on a Mac and zip-uploading `cryptography` is a classic foot-gun. Build in a Docker image matching `public.ecr.aws/lambda/python:3.13`.
|
||||
26. **arm64 saves ~20 % at the same memory** but *some* wheels are still x86-only. Audit your deps before flipping the architecture.
|
||||
27. **Layers are merge-ordered; later layers overwrite earlier.** A "base" layer for your shared dependencies works; conflicting layers silently shadow each other.
|
||||
28. **Container-image deploys are cached on the Lambda host.** First cold start can be slow (image pull); subsequent are normal. Keep images small even though the limit is 10 GB.
|
||||
|
||||
## Time, scheduling, secrets
|
||||
|
||||
29. **EventBridge schedule (cron/rate) is always UTC.** "9 AM" in your local time means something different in production. Use the new EventBridge Scheduler (2022) for time-zone-aware schedules.
|
||||
30. **Async invocations have a 6-hour event age.** If retries fail past that, the event is silently dropped unless you've set a DLQ or on-failure destination.
|
||||
31. **Secrets in env vars are visible to anyone with `lambda:GetFunctionConfiguration`.** Encrypted at rest, plaintext in the console. Use Secrets Manager / Parameter Store for actual secrets.
|
||||
|
||||
> ⚠️ **Skim test:** if you can re-state the cold-start split (Init / Handler), the 6 MB / 256 KB / 4 KB / 250 MB / 10 GB constants, and the difference between resource policy and execution role from memory, you'll handle most "tell me about Lambda" interview questions.
|
||||
40
docs/lambdas-md/lambda-17-adjacent.md
Normal file
40
docs/lambdas-md/lambda-17-adjacent.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Adjacent
|
||||
|
||||
> Brief orientation on AWS Glue and Prometheus/Grafana — the secondary gaps from the interview.
|
||||
|
||||
## AWS Glue
|
||||
|
||||
Glue is a managed Spark-based ETL service. Lambda and Glue solve different problems:
|
||||
|
||||
| | Lambda | Glue |
|
||||
|--|--------|------|
|
||||
| **Runtime model** | Serverless; up to 15 min; one handler at a time per env | Managed Spark cluster; hours-long jobs; distributed compute |
|
||||
| **Data scale** | Up to a few GB comfortably | TB to PB natively |
|
||||
| **Language** | Python, Node, Java, Go, custom runtime | PySpark, Scala; Glue Studio for no-code |
|
||||
| **Startup time** | Milliseconds (warm) | 1–2 minutes to provision Spark cluster |
|
||||
| **Cost model** | Per request + per ms | Per DPU-hour (1 DPU = $0.44/hr); 10-minute minimum billing |
|
||||
| **Use for** | Light transforms, event reactions, API backends | Large-scale joins, aggregations, schema inference on data lake |
|
||||
|
||||
Key Glue concepts to know: **DynamicFrame** (Glue's DataFrame variant with schema flexibility), **Glue Catalog** (centralised metadata store for table schemas — also used by Athena), **Job Bookmarks** (Glue tracks processed S3 partitions to avoid reprocessing on incremental runs).
|
||||
|
||||
The decision is usually straightforward: if the data fits in Lambda's memory and the job finishes in under 15 minutes, use Lambda. If you're joining multiple large S3 datasets or transforming daily partition files, use Glue.
|
||||
|
||||
## Prometheus
|
||||
|
||||
Prometheus is a pull-based time-series metrics system. It scrapes HTTP `/metrics` endpoints on a schedule. The fundamental tension with Lambda: Lambda functions are ephemeral — there's no persistent HTTP endpoint to scrape, and the function may be at zero concurrency between invocations.
|
||||
|
||||
Options for Lambda → Prometheus:
|
||||
|
||||
- **EMF → CloudWatch → Grafana CloudWatch plugin** — no Prometheus involved. Grafana reads directly from CloudWatch. Easiest for AWS-native stacks.
|
||||
- **Remote write to Amazon Managed Prometheus (AMP)** — the function pushes metrics to AMP via the Prometheus remote_write API at the end of each invocation. Grafana or Amazon Managed Grafana reads from AMP. Requires the `prometheus_client` library and SIGV4 signing on the remote_write request.
|
||||
- **Push gateway** — a persistent intermediate that Lambda pushes to; Prometheus scrapes the gateway. More infrastructure to manage, stale metric risk if the push gateway isn't flushed between invocations.
|
||||
|
||||
## Grafana
|
||||
|
||||
Grafana is a dashboarding layer — it doesn't store data, it queries data sources. Relevant data sources for Lambda observability:
|
||||
|
||||
- **CloudWatch** — built-in Grafana plugin; queries CW Metrics and CW Logs Insights. Zero extra infrastructure. The standard choice for Lambda metrics (invocations, errors, duration, throttles, concurrent executions).
|
||||
- **Amazon Managed Prometheus** — query via PromQL if you've pushed custom metrics.
|
||||
- **Amazon Managed Grafana (AMG)** — Grafana-as-a-service; integrates with AWS IAM; auto-discovers CW namespaces. Avoids self-hosting Grafana.
|
||||
|
||||
For a Lambda-only stack with no existing Prometheus investment, the practical answer is: use EMF for custom metrics, use CloudWatch for the built-in Lambda metrics, and connect Grafana to CloudWatch. It requires no extra infrastructure and gives you dashboards in an hour.
|
||||
80
docs/lambdas-md/lambda-18-labs.md
Normal file
80
docs/lambdas-md/lambda-18-labs.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Labs
|
||||
|
||||
> Hands-on walkthroughs that modify the existing app. Each mutates what you already have — no throw-away exercises.
|
||||
|
||||
## Lab 0 — Local sandbox (start here)
|
||||
|
||||
**Goal:** run the full stack locally against MinIO with real PDFs.
|
||||
|
||||
1. `make install` — creates `.venv` and installs deps
|
||||
2. `make up` — starts MinIO on :9000 (API) and :9001 (console)
|
||||
3. `SOURCE_DIR=~/path/to/pdfs make seed` — uploads PDFs to MinIO bucket
|
||||
4. `make invoke` — runs `invoke.py` which calls `handler()` with a minimal event
|
||||
5. Open `http://localhost:9001` (minioadmin/minioadmin) and find the generated manifest in the `manifests/` prefix
|
||||
|
||||
**What you can break:** set `PREFIX` to a non-existent prefix and observe the handler returns count=0. Set `QUEUE_MAX=1` and observe the backpressure on the producer. Remove `S3_ENDPOINT_URL` and watch it fail to connect.
|
||||
|
||||
## Lab 1 — Deploy to real AWS
|
||||
|
||||
**Goal:** package and deploy the function to AWS Lambda, invoke it against a real S3 bucket.
|
||||
|
||||
1. Create an S3 bucket and upload sample PDFs to `2026/04/` prefix
|
||||
2. Create an IAM execution role with `s3:GetObject`, `s3:PutObject`, `s3:ListBucket`, and `logs:*`
|
||||
3. Build the deployment zip inside the Lambda image:
|
||||
`docker run --rm -v $PWD:/var/task public.ecr.aws/lambda/python:3.13 pip install -r requirements.txt -t package/`
|
||||
4. Create the function: `aws lambda create-function --handler lambda_function.handler …`
|
||||
5. Invoke: `aws lambda invoke --function-name pdf-scanner --payload '{}' out.json`
|
||||
6. Verify the manifest appeared in S3 and the presigned URL works
|
||||
|
||||
**What you can break:** invoke without `s3:ListBucket` on the bucket (not the object ARN) — observe AccessDenied. Watch CloudTrail to see the denied call.
|
||||
|
||||
## Lab 2 — Add an S3 trigger
|
||||
|
||||
**Goal:** make the function fire automatically when a PDF is uploaded.
|
||||
|
||||
1. Add a resource policy entry granting S3 `lambda:InvokeFunction`
|
||||
2. Configure an S3 event notification on the bucket for `s3:ObjectCreated:*` filtered to `*.pdf`
|
||||
3. Upload a PDF and check CloudWatch Logs for the invocation
|
||||
4. Notice the event structure differs from the manual invoke — update the handler to extract the key from `event["Records"][0]["s3"]["object"]["key"]`
|
||||
|
||||
**What you can break:** upload a non-PDF to the same prefix and verify the filter prevents invocation. Remove the resource policy and verify the trigger silently stops firing (no error to the uploader — this is the async invocation model).
|
||||
|
||||
## Lab 3 — Switch to arm64
|
||||
|
||||
**Goal:** migrate to Graviton2 and verify 20% cost reduction.
|
||||
|
||||
1. Rebuild the zip using the arm64 Lambda image: `public.ecr.aws/lambda/python:3.13-arm64`
|
||||
2. Update the function architecture: `aws lambda update-function-configuration --architectures arm64`
|
||||
3. Update the function code with the arm64 zip
|
||||
4. Invoke and compare REPORT duration and billed duration in CloudWatch
|
||||
|
||||
**What you can break:** try deploying the x86 zip against the arm64 architecture — the function will import-error on any C-extension wheels.
|
||||
|
||||
## Lab 4 — Enable Provisioned Concurrency
|
||||
|
||||
**Goal:** eliminate cold starts on the production alias.
|
||||
|
||||
1. Publish version 1: `aws lambda publish-version --function-name pdf-scanner`
|
||||
2. Create alias `prod` pointing to version 1
|
||||
3. Enable PC: `aws lambda put-provisioned-concurrency-config --function-name pdf-scanner --qualifier prod --provisioned-concurrent-executions 2`
|
||||
4. Invoke via the alias ARN and confirm `Init Duration` is absent from REPORT lines
|
||||
5. Check your AWS bill after 1 hour — note the PC charges
|
||||
|
||||
## Lab 5 — Add X-Ray tracing
|
||||
|
||||
**Goal:** see a trace with S3 subsegments in the X-Ray console.
|
||||
|
||||
1. Add `aws-xray-sdk` to `requirements.txt` and rebuild the zip
|
||||
2. Add to `lambda_function.py`: `from aws_xray_sdk.core import patch_all; patch_all()`
|
||||
3. Enable active tracing on the function and add X-Ray permissions to the execution role
|
||||
4. Invoke and open X-Ray → Traces in the console — verify S3 `list_objects_v2` and `generate_presigned_url` appear as subsegments
|
||||
|
||||
## Lab 6 — Fan out with Step Functions
|
||||
|
||||
**Goal:** process multiple S3 prefixes in parallel using a Map state.
|
||||
|
||||
1. Update the handler to accept a `prefix` key in the event (instead of reading from env var)
|
||||
2. Create a Step Functions state machine with a Map state that iterates over a list of prefixes and invokes the Lambda for each
|
||||
3. Start an execution with input: `{"prefixes": ["2026/01/", "2026/02/", "2026/03/"]}`
|
||||
4. Observe parallel Lambda invocations in the execution graph and CloudWatch
|
||||
5. Add error handling: configure the Map state to catch Lambda errors and continue rather than fail the whole execution
|
||||
258
docs/lambdas-md/lambda-19-repository.md
Normal file
258
docs/lambdas-md/lambda-19-repository.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Repository
|
||||
|
||||
> Tree of `eth/` — the sandbox plus this study site.
|
||||
|
||||
```
|
||||
eth/
|
||||
├── lambda_function.py — handler: async PDF scan → presigned URLs → JSONL manifest
|
||||
├── invoke.py — local runner: calls handler() with a minimal event, prints result
|
||||
├── seed.py — uploads PDFs from a local directory to MinIO
|
||||
├── requirements.txt — aioboto3, aiofiles (+ transitive: aiobotocore, botocore…)
|
||||
├── docker-compose.yml — runs MinIO on :9000 (S3 API) and :9001 (web console)
|
||||
├── Makefile — install / up / down / seed / invoke / graphs / docs
|
||||
├── def/
|
||||
│ └── task.md — original interview exercise specification
|
||||
└── docs/
|
||||
├── index.html — this study site (single-page, no build step)
|
||||
├── viewer.html — pan/zoom SVG viewer (opened by graph links)
|
||||
└── graphs/
|
||||
├── system_overview.dot / .svg — caller → handler → MinIO/S3 → manifest
|
||||
├── lifecycle.dot / .svg — init / handler / freeze / thaw / shutdown
|
||||
└── cold_warm_timeline.dot / .svg — cold vs warm invocation timeline
|
||||
```
|
||||
|
||||
## Walking through lambda_function.py
|
||||
|
||||
### What the function does, end to end
|
||||
|
||||
In one paragraph: the function lists every PDF inside an S3 prefix. For each one, it generates a presigned download URL that expires in 15 minutes. It writes those (key, URL) pairs into a JSONL file in `/tmp` as it goes. When the listing is done, it uploads the JSONL to S3 as a manifest, generates one more presigned URL pointing to the manifest itself, deletes the local file, and returns the manifest URL plus the count.
|
||||
|
||||
The use case: you want to ship a batch of files to someone who isn't on your AWS account. Send them one URL. They open it, get back a list of links, every link works for 15 minutes, then everything dies.
|
||||
|
||||
### Imports and module-scope config
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aioboto3
|
||||
import aiofiles
|
||||
```
|
||||
|
||||
`aioboto3` is the async version of boto3 — async S3 calls, so we can overlap I/O. `aiofiles` is async filesystem access — same reason.
|
||||
|
||||
```python
|
||||
BUCKET = os.environ.get("BUCKET_NAME", "my-company-reports-bucket")
|
||||
PREFIX = os.environ.get("PREFIX", "2026/04/")
|
||||
EXPIRY = int(os.environ.get("URL_EXPIRY_SECONDS", "900"))
|
||||
ENDPOINT = os.environ.get("S3_ENDPOINT_URL") or None
|
||||
QUEUE_MAX = int(os.environ.get("QUEUE_MAX", "2000"))
|
||||
|
||||
_DONE = object()
|
||||
```
|
||||
|
||||
Five environment reads at module scope — init phase. They run once per cold start, get cached as Python module attributes, and every warm invocation reuses them for free.
|
||||
|
||||
`ENDPOINT` is the trick that lets this run against MinIO locally. When you run on real Lambda, you don't set the env var, the value is `None`, and aioboto3 talks to real S3. When you run locally, you set it to `http://localhost:9000` and the same code talks to MinIO. The function doesn't know the difference.
|
||||
|
||||
`_DONE` is a sentinel — a unique singleton put on the queue to signal "no more items coming." The reason it's an `object()` and not a string: a string could theoretically collide with a real S3 key. An `object()` instance has a unique identity; comparing with `is` — not `==` — is unambiguous.
|
||||
|
||||
### The handler — minimal on purpose
|
||||
|
||||
```python
|
||||
def handler(event, context):
|
||||
result = asyncio.run(_run())
|
||||
return {"statusCode": 200, "body": json.dumps(result)}
|
||||
```
|
||||
|
||||
The handler is sync because Lambda's contract is sync. AWS calls `handler(event, context)` and waits for it to return. Inside, `asyncio.run` opens a fresh event loop, runs the async coroutine, gets back a result. The API-Gateway-style response shape (`statusCode` + `body`) is a habit — useful when the function gets fronted by API Gateway later; a pure Lambda invoke doesn't need it but it doesn't hurt.
|
||||
|
||||
`asyncio.run` creates a fresh event loop per invocation. This means async clients can't be shared across invocations the way sync boto3 clients can. The cost is small — tens of microseconds — but it's the reason the S3 client is created inside `_run`, not at module scope.
|
||||
|
||||
Why async at all? Lambda bills per millisecond of wall-clock time. Anything you can overlap, you save money on. The function does a lot of S3 calls — listing pages, generating presigned URLs, writing files. While S3 is preparing the next page of results, the consumer is already presigning and writing the previous page. That overlap directly reduces duration and cost.
|
||||
|
||||
### `_run()` — the actual work
|
||||
|
||||
```python
|
||||
async def _run():
|
||||
session = aioboto3.Session()
|
||||
async with session.client("s3", endpoint_url=ENDPOINT) as s3:
|
||||
queue: asyncio.Queue = asyncio.Queue(maxsize=QUEUE_MAX)
|
||||
manifest_path = f"/tmp/{uuid.uuid4()}.jsonl"
|
||||
```
|
||||
|
||||
The session is created inside `_run`, not at module scope, because aioboto3 async clients are tied to the event loop — and each invocation gets a fresh event loop via `asyncio.run`. Sync boto3 clients you'd put at module scope; async ones you create per invocation.
|
||||
|
||||
The queue has a maximum size of 2000 by default. Without the bound, if the producer is faster than the consumer, the queue grows in memory. Lambda has at most 10 GB of memory, usually 256–512 MB. Scanning a bucket with a million PDFs and loading them all before presigning even one would OOM. The bounded queue gives backpressure: when full, `await queue.put(...)` blocks until the consumer takes something off. Memory stays flat.
|
||||
|
||||
The manifest path uses a UUID so that back-to-back warm invocations on the same environment don't collide on `/tmp`. (`/tmp` persists across warm invocations; a fixed filename would be a race condition.)
|
||||
|
||||
### The producer
|
||||
|
||||
```python
|
||||
async def producer():
|
||||
paginator = s3.get_paginator("list_objects_v2")
|
||||
async for page in paginator.paginate(Bucket=BUCKET, Prefix=PREFIX):
|
||||
for obj in page.get("Contents", []) or []:
|
||||
key = obj["Key"]
|
||||
if key.lower().endswith(".pdf"):
|
||||
await queue.put(key)
|
||||
await queue.put(_DONE)
|
||||
```
|
||||
|
||||
Defined inside `_run` as a closure — captures `s3` and `queue` from the enclosing scope without arguments. Also signals it's a private implementation detail.
|
||||
|
||||
S3 returns at most 1000 objects per page. The paginator hides the pagination — `async for page in paginator.paginate(...)` transparently fetches the next page when needed. For each object, filter by `.pdf` (case-insensitive) and put the key on the queue.
|
||||
|
||||
When the paginator is exhausted, put `_DONE` on the queue. That tells the consumer to stop. `asyncio.Queue` has no close method — the sentinel is the standard pattern.
|
||||
|
||||
`await queue.put(key)` blocks if the queue is full. That's the backpressure: producer pauses until consumer takes something off.
|
||||
|
||||
### The consumer
|
||||
|
||||
```python
|
||||
async def consumer():
|
||||
count = 0
|
||||
async with aiofiles.open(manifest_path, "w") as f:
|
||||
while True:
|
||||
item = await queue.get()
|
||||
if item is _DONE:
|
||||
break
|
||||
url = await s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": BUCKET, "Key": item},
|
||||
ExpiresIn=EXPIRY,
|
||||
)
|
||||
await f.write(json.dumps({"key": item, "url": url}) + "\n")
|
||||
count += 1
|
||||
return count
|
||||
```
|
||||
|
||||
Same closure pattern. Opens the manifest file async. Loops forever, pulling from the queue. On sentinel, breaks. Otherwise generates a presigned URL and writes a JSONL line.
|
||||
|
||||
`generate_presigned_url` is a **local computation**, not a network call. It uses your credentials, bucket, key, expiry, and region to produce a signed URL deterministically. Fast — no HTTP request.
|
||||
|
||||
Why JSONL instead of a JSON array? Because JSONL streams. You write one line at a time without buffering the whole array in memory. The reader can process one line at a time. If the manifest grows to gigabytes, JSONL stays usable.
|
||||
|
||||
### Running them together
|
||||
|
||||
```python
|
||||
prod_task = asyncio.create_task(producer())
|
||||
count = await consumer()
|
||||
await prod_task
|
||||
```
|
||||
|
||||
`create_task` schedules the producer on the event loop and returns immediately — producer runs in the background. `await consumer()` runs the consumer in the foreground until it sees the sentinel and returns the count. `await prod_task` ensures the producer has fully completed and propagates any exceptions.
|
||||
|
||||
This is the overlap: while S3 is preparing the next LIST page (network round trip), the consumer is presigning and writing the previous page. Sequential would be: list everything, then presign everything. With overlap you pay only the larger of the two latencies. For thousands of files, this cuts wall-clock time and cost noticeably.
|
||||
|
||||
### Uploading the manifest
|
||||
|
||||
```python
|
||||
manifest_key = f"manifests/{uuid.uuid4()}.jsonl"
|
||||
async with aiofiles.open(manifest_path, "rb") as f:
|
||||
body = await f.read()
|
||||
await s3.put_object(
|
||||
Bucket=BUCKET,
|
||||
Key=manifest_key,
|
||||
Body=body,
|
||||
ContentType="application/x-ndjson",
|
||||
)
|
||||
```
|
||||
|
||||
Read the `/tmp` file as bytes and upload with `put_object`. Content type `application/x-ndjson` is the registered MIME type for newline-delimited JSON. `put_object` rather than `upload_file` because aioboto3's async multipart logic is simpler this way for files in the hundreds-of-KB to few-MB range.
|
||||
|
||||
### Generating the manifest URL and cleaning up
|
||||
|
||||
```python
|
||||
manifest_url = await s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": BUCKET, "Key": manifest_key},
|
||||
ExpiresIn=EXPIRY,
|
||||
)
|
||||
|
||||
os.unlink(manifest_path)
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"manifest_key": manifest_key,
|
||||
"manifest_url": manifest_url,
|
||||
}
|
||||
```
|
||||
|
||||
Presign the manifest itself. Delete the `/tmp` file — `/tmp` persists across warm invocations; without cleanup, a thousand invocations on the same environment would fill it. Return count, S3 key, and the URL. The handler wraps that in `{"statusCode": 200, "body": ...}` and returns.
|
||||
|
||||
### Why this design?
|
||||
|
||||
**Why presigned URLs, not return the data directly?** The response is small (one URL), the recipient doesn't need an AWS account to use it, and it expires automatically. The URL is signed by your credentials and works for anyone who has it for 15 minutes.
|
||||
|
||||
**Why upload the manifest to S3 and return a URL to it, instead of returning the manifest contents inline?** The 6 MB sync response cap. Ten thousand presigned URLs in JSONL is 3–5 MB. Twenty thousand blows the cap — silently: the function succeeds, the caller gets a 413. The manifest-in-S3 pattern has no upper bound.
|
||||
|
||||
**Why async?** Overlap S3 LIST calls with presigning and file writes. Even though presigning is local, the LIST round trips and final upload benefit from non-blocking I/O.
|
||||
|
||||
**Why producer and consumer instead of one loop?** The producer is bursty (up to 1000 keys per page dump). The consumer is steady. Decoupling with a queue means the producer races ahead while the consumer drains, instead of LIST → presign → LIST → presign serially.
|
||||
|
||||
**Why a bounded queue?** Backpressure. Without the bound, the producer can outrun the consumer and exhaust memory. With the bound, `await queue.put(...)` blocks when full. Memory stays flat regardless of bucket size.
|
||||
|
||||
**Why a sentinel and not closing the queue?** `asyncio.Queue` has no close method. The sentinel is the standard "done" signal.
|
||||
|
||||
**Why nested functions?** Closures over `s3`, `queue`, `manifest_path`. No arguments to pass. Private implementation details of `_run`.
|
||||
|
||||
**Why UUID in the `/tmp` filename?** `/tmp` persists across warm invocations. A fixed filename collides between back-to-back runs. UUID guarantees uniqueness.
|
||||
|
||||
**Why `_DONE = object()` instead of a string sentinel?** An `object()` instance has a unique identity that can't possibly collide with any real S3 key. `is` comparison (identity, not equality) is unambiguous.
|
||||
|
||||
**Why `os.unlink` at the end?** `/tmp` is per-environment, at most 10 GB, and persists. A thousand warm invocations without cleanup would fill it and crash subsequent runs.
|
||||
|
||||
### Cold start vs warm — what you'd see in CloudWatch
|
||||
|
||||
First invocation (cold):
|
||||
```
|
||||
REPORT RequestId: ... Duration: 312.45 ms Billed Duration: 313 ms
|
||||
Memory Size: 256 MB Max Memory Used: 89 MB
|
||||
Init Duration: 423.12 ms
|
||||
```
|
||||
|
||||
`Init Duration` ~400 ms covers importing aioboto3 and aiofiles (aioboto3 pulls in aiobotocore which pulls in botocore — heavy). `Duration` ~300 ms is the actual scan: list, presign, write, upload.
|
||||
|
||||
Second invocation within 30 seconds (warm):
|
||||
```
|
||||
REPORT RequestId: ... Duration: 287.91 ms Billed Duration: 288 ms
|
||||
Memory Size: 256 MB Max Memory Used: 91 MB
|
||||
```
|
||||
|
||||
No `Init Duration` line. Jumped straight to the handler. ~30 ms saved. For a function that runs once a day, every invocation is cold and init matters. For one that runs every few seconds, almost everything is warm.
|
||||
|
||||
### What happens if it times out
|
||||
|
||||
The default function timeout is 3 s — almost certainly not enough. Set it explicitly to 30–60 s for a small prefix, up to 900 s (15 min) for a large one. If it times out, Lambda kills the process. The `/tmp` file may not have been deleted. The manifest may or may not have been uploaded. Re-running produces a fresh manifest with new UUIDs — the previous partial manifest stays in S3 until TTL or manual cleanup.
|
||||
|
||||
### How would you scale this
|
||||
|
||||
**Fan out by prefix.** Wrap in a Step Functions Map state. Pass a list of prefixes; each map iteration runs one Lambda for one prefix. `MaxConcurrency` controls parallelism without saturating the account concurrency quota.
|
||||
|
||||
**Go event-driven.** Subscribe to S3 `ObjectCreated` events filtered to `*.pdf`. The function fires once per upload, handles one file at a time. No producer/consumer needed — nothing to enumerate. Simpler, but different semantics: "process new files as they arrive" vs "scan the existing bucket."
|
||||
|
||||
### What I'd change before production
|
||||
|
||||
1. **Move `BUCKET` and `PREFIX` to the event payload** — currently set at deploy time, which means one function per prefix. Event-driven config lets one function serve many prefixes.
|
||||
2. **Structured logging** — JSON to stdout with `request_id`, `bucket`, `prefix`, `count`. Logs Insights can aggregate without regex.
|
||||
3. **EMF metric for `count`** — free CloudWatch metric, no additional API call. Dashboard "PDFs processed per invocation" over time.
|
||||
4. **Producer error handling** — if `paginator.paginate` raises, the producer task fails but the consumer keeps blocking on `queue.get()` forever, and the function times out. Wrap the producer body in `try/finally` that always puts `_DONE` on the queue so the consumer exits cleanly.
|
||||
5. **Explicit timeout on `queue.get()`** — `await asyncio.wait_for(queue.get(), timeout=X)` prevents the consumer hanging indefinitely if the producer dies without putting the sentinel.
|
||||
6. **Consider sync boto3** — `aioboto3` adds ~200 ms to the cold start. If cold start matters and file counts are small, sync boto3 with threading is simpler and starts faster. Async pays off when file counts are large enough that overlap is significant.
|
||||
|
||||
## Makefile targets
|
||||
|
||||
| Target | What it does |
|
||||
|--------|--------------|
|
||||
| `make install` | Creates `.venv`, installs `requirements.txt` |
|
||||
| `make up` | Starts MinIO via `docker compose up -d` |
|
||||
| `make down` | Stops MinIO (keeps volumes) |
|
||||
| `make clean` | Stops MinIO and deletes volumes (wipes bucket data) |
|
||||
| `SOURCE_DIR=path make seed` | Uploads all files from `path` to MinIO |
|
||||
| `make invoke` | Runs `invoke.py` (calls `handler()` directly) |
|
||||
| `make graphs` | Renders all `docs/graphs/*.dot` → `.svg` via Graphviz `dot` |
|
||||
| `make docs` | Renders graphs then opens `docs/index.html` |
|
||||
40
docs/lambdas-md/lambda-README.md
Normal file
40
docs/lambdas-md/lambda-README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# AWS Lambda — Study notes & sandbox
|
||||
|
||||
Study site built on top of a working Lambda + MinIO sandbox. Read the page, run the code, break things on purpose.
|
||||
|
||||
## Foundations
|
||||
|
||||
- [01 — Overview](lambda-01-overview.md)
|
||||
- [02 — Mental Model](lambda-02-mental-model.md)
|
||||
- [03 — Limits](lambda-03-limits.md)
|
||||
|
||||
## Operating
|
||||
|
||||
- [04 — Cold Starts](lambda-04-cold-starts.md)
|
||||
- [05 — Concurrency](lambda-05-concurrency.md)
|
||||
- [06 — Triggers](lambda-06-triggers.md)
|
||||
- [07 — IAM & Permissions](lambda-07-iam.md)
|
||||
- [08 — Packaging](lambda-08-packaging.md)
|
||||
- [09 — VPC & Networking](lambda-09-vpc-networking.md)
|
||||
|
||||
## Production
|
||||
|
||||
- [10 — Observability](lambda-10-observability.md)
|
||||
- [11 — Async & Errors](lambda-11-async-errors.md)
|
||||
- [12 — Step Functions](lambda-12-step-functions.md)
|
||||
- [13 — Cost](lambda-13-cost.md)
|
||||
- [14 — Local Dev](lambda-14-local-dev.md)
|
||||
- [15 — CI/CD](lambda-15-cicd.md)
|
||||
|
||||
## Reference
|
||||
|
||||
- [16 — Pitfalls](lambda-16-pitfalls.md)
|
||||
- [17 — Adjacent](lambda-17-adjacent.md)
|
||||
- [18 — Labs](lambda-18-labs.md)
|
||||
- [19 — Repository](lambda-19-repository.md)
|
||||
|
||||
## Graphs
|
||||
|
||||
- [System overview](lambda-system_overview.svg)
|
||||
- [Lifecycle](lambda-lifecycle.svg)
|
||||
- [Cold vs warm timeline](lambda-cold_warm_timeline.svg)
|
||||
158
docs/lambdas-md/lambda-cold_warm_timeline.svg
Normal file
158
docs/lambdas-md/lambda-cold_warm_timeline.svg
Normal file
@@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: cold_warm_timeline Pages: 1 -->
|
||||
<svg width="1511pt" height="223pt"
|
||||
viewBox="0.00 0.00 1511.00 223.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 219.38)">
|
||||
<title>cold_warm_timeline</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-219.38 1506.75,-219.38 1506.75,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="751.38" y="-196.18" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Cold vs warm — what gets billed, what gets measured</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_cold</title>
|
||||
<polygon fill="#0a0e17" stroke="#ff3d00" stroke-dasharray="5,2" points="8,-8 8,-127 519.75,-127 519.75,-8 8,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="263.88" y="-107.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#ff3d00">INVOCATION 1 — cold (Init Duration shows in CloudWatch)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_warm1</title>
|
||||
<polygon fill="#0a0e17" stroke="#00c853" stroke-dasharray="5,2" points="611,-22 611,-114 1017,-114 1017,-22 611,-22"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="814" y="-94.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#00c853">INVOCATION 2 — warm (no Init Duration logged)</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_warm2</title>
|
||||
<polygon fill="#0a0e17" stroke="#00c853" stroke-dasharray="5,2" points="1038,-28 1038,-108 1494.75,-108 1494.75,-28 1038,-28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1266.38" y="-88.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#00c853">INVOCATION 3 — warm</text>
|
||||
</g>
|
||||
<!-- c_dl -->
|
||||
<g id="node1" class="node">
|
||||
<title>c_dl</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="116,-78.25 16,-78.25 16,-29.75 116,-29.75 116,-78.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="66" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">Download code</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="66" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">~50–200 ms</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="66" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">(NOT billed)</text>
|
||||
</g>
|
||||
<!-- c_init -->
|
||||
<g id="node2" class="node">
|
||||
<title>c_init</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="306.25,-91.75 153,-91.75 153,-16.25 306.25,-16.25 306.25,-91.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-77.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">Init phase</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">~200–800 ms typical</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(boto3/aioboto3 imports,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">client build)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="229.62" y="-23.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(billed at full mem)</text>
|
||||
</g>
|
||||
<!-- c_dl->c_init -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>c_dl->c_init</title>
|
||||
<path fill="none" stroke="#4a5568" d="M116.37,-54C124.38,-54 132.87,-54 141.45,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="141.25,-57.5 151.25,-54 141.25,-50.5 141.25,-57.5"/>
|
||||
</g>
|
||||
<!-- c_handler -->
|
||||
<g id="node3" class="node">
|
||||
<title>c_handler</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="420.75,-78.25 343.25,-78.25 343.25,-29.75 420.75,-29.75 420.75,-78.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="382" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">Handler</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="382" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">~5–500 ms</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="382" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(billed)</text>
|
||||
</g>
|
||||
<!-- c_init->c_handler -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>c_init->c_handler</title>
|
||||
<path fill="none" stroke="#4a5568" d="M306.69,-54C315.11,-54 323.52,-54 331.48,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="331.28,-57.5 341.28,-54 331.28,-50.5 331.28,-57.5"/>
|
||||
</g>
|
||||
<!-- c_freeze -->
|
||||
<g id="node4" class="node">
|
||||
<title>c_freeze</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="511.75,-72 457.75,-72 457.75,-36 511.75,-36 511.75,-72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="484.75" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">freeze</text>
|
||||
</g>
|
||||
<!-- c_handler->c_freeze -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>c_handler->c_freeze</title>
|
||||
<path fill="none" stroke="#4a5568" d="M421.09,-54C429.25,-54 437.87,-54 446.02,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="446,-57.5 456,-54 446,-50.5 446,-57.5"/>
|
||||
</g>
|
||||
<!-- w_thaw -->
|
||||
<g id="node5" class="node">
|
||||
<title>w_thaw</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="756.62,-78.25 664.88,-78.25 664.88,-29.75 756.62,-29.75 756.62,-78.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="710.75" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">thaw</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="710.75" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">microseconds</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="710.75" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">(NOT billed)</text>
|
||||
</g>
|
||||
<!-- c_freeze->w_thaw -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>c_freeze->w_thaw</title>
|
||||
<path fill="none" stroke="#00c853" d="M511.88,-54C546.28,-54 607.68,-54 652.99,-54"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="652.92,-57.5 662.92,-54 652.92,-50.5 652.92,-57.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="565.38" y="-67.95" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">next event</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="565.38" y="-56.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">(< idle window)</text>
|
||||
</g>
|
||||
<!-- w_handler -->
|
||||
<g id="node6" class="node">
|
||||
<title>w_handler</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="871.12,-78.25 793.62,-78.25 793.62,-29.75 871.12,-29.75 871.12,-78.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="832.38" y="-63.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">Handler</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="832.38" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">~5–500 ms</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="832.38" y="-36.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">(billed)</text>
|
||||
</g>
|
||||
<!-- w_thaw->w_handler -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>w_thaw->w_handler</title>
|
||||
<path fill="none" stroke="#4a5568" d="M756.98,-54C765.16,-54 773.73,-54 782.03,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="781.99,-57.5 791.99,-54 781.99,-50.5 781.99,-57.5"/>
|
||||
</g>
|
||||
<!-- w_freeze -->
|
||||
<g id="node7" class="node">
|
||||
<title>w_freeze</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="962.12,-72 908.12,-72 908.12,-36 962.12,-36 962.12,-72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="935.12" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">freeze</text>
|
||||
</g>
|
||||
<!-- w_handler->w_freeze -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>w_handler->w_freeze</title>
|
||||
<path fill="none" stroke="#4a5568" d="M871.46,-54C879.63,-54 888.25,-54 896.39,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="896.37,-57.5 906.37,-54 896.37,-50.5 896.37,-57.5"/>
|
||||
</g>
|
||||
<!-- w2_thaw -->
|
||||
<g id="node8" class="node">
|
||||
<title>w2_thaw</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="1100,-72 1046,-72 1046,-36 1100,-36 1100,-72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1073" y="-50.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#8892a8">thaw</text>
|
||||
</g>
|
||||
<!-- w_freeze->w2_thaw -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>w_freeze->w2_thaw</title>
|
||||
<path fill="none" stroke="#00c853" d="M962.32,-54C982.81,-54 1011.57,-54 1034.5,-54"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="1034.37,-57.5 1044.37,-54 1034.37,-50.5 1034.37,-57.5"/>
|
||||
</g>
|
||||
<!-- w2_handler -->
|
||||
<g id="node9" class="node">
|
||||
<title>w2_handler</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="1486.75,-72 1428,-72 1428,-36 1486.75,-36 1486.75,-72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1457.38" y="-57.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">Handler</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1457.38" y="-43.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">(billed)</text>
|
||||
</g>
|
||||
<!-- w2_thaw->w2_handler -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>w2_thaw->w2_handler</title>
|
||||
<path fill="none" stroke="#4a5568" d="M1100.28,-54C1166,-54 1337.82,-54 1416.27,-54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1416.07,-57.5 1426.07,-54 1416.07,-50.5 1416.07,-57.5"/>
|
||||
</g>
|
||||
<!-- notes -->
|
||||
<g id="node10" class="node">
|
||||
<title>notes</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" points="1404,-187.88 1118,-187.88 1118,-116.12 1410,-116.12 1410,-181.88 1404,-187.88"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="1404,-187.88 1404,-181.88"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="1410,-181.88 1404,-181.88"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-174.38" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">Init Duration is ONLY in cold-start logs.</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-161.62" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">Duration is the handler portion only.</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-148.88" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">Billed Duration rounds Duration up to 1 ms.</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-136.12" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">With Provisioned Concurrency, init runs ahead of time —</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1264" y="-123.38" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#b4bccf">you pay for it in PC pricing, not per invocation.</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
79
docs/lambdas-md/lambda-function.py
Normal file
79
docs/lambdas-md/lambda-function.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aioboto3
|
||||
import aiofiles
|
||||
|
||||
BUCKET = os.environ.get("BUCKET_NAME", "my-company-reports-bucket")
|
||||
PREFIX = os.environ.get("PREFIX", "2026/04/")
|
||||
EXPIRY = int(os.environ.get("URL_EXPIRY_SECONDS", "900"))
|
||||
ENDPOINT = os.environ.get("S3_ENDPOINT_URL") or None
|
||||
QUEUE_MAX = int(os.environ.get("QUEUE_MAX", "2000"))
|
||||
|
||||
_DONE = object()
|
||||
|
||||
|
||||
async def _run():
|
||||
session = aioboto3.Session()
|
||||
async with session.client("s3", endpoint_url=ENDPOINT) as s3:
|
||||
queue: asyncio.Queue = asyncio.Queue(maxsize=QUEUE_MAX)
|
||||
manifest_path = f"/tmp/{uuid.uuid4()}.jsonl"
|
||||
|
||||
async def producer():
|
||||
paginator = s3.get_paginator("list_objects_v2")
|
||||
async for page in paginator.paginate(Bucket=BUCKET, Prefix=PREFIX):
|
||||
for obj in page.get("Contents", []) or []:
|
||||
key = obj["Key"]
|
||||
if key.lower().endswith(".pdf"):
|
||||
await queue.put(key)
|
||||
await queue.put(_DONE)
|
||||
|
||||
async def consumer():
|
||||
count = 0
|
||||
async with aiofiles.open(manifest_path, "w") as f:
|
||||
while True:
|
||||
item = await queue.get()
|
||||
if item is _DONE:
|
||||
break
|
||||
url = await s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": BUCKET, "Key": item},
|
||||
ExpiresIn=EXPIRY,
|
||||
)
|
||||
await f.write(json.dumps({"key": item, "url": url}) + "\n")
|
||||
count += 1
|
||||
return count
|
||||
|
||||
prod_task = asyncio.create_task(producer())
|
||||
count = await consumer()
|
||||
await prod_task
|
||||
|
||||
manifest_key = f"manifests/{uuid.uuid4()}.jsonl"
|
||||
async with aiofiles.open(manifest_path, "rb") as f:
|
||||
body = await f.read()
|
||||
await s3.put_object(
|
||||
Bucket=BUCKET,
|
||||
Key=manifest_key,
|
||||
Body=body,
|
||||
ContentType="application/x-ndjson",
|
||||
)
|
||||
manifest_url = await s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": BUCKET, "Key": manifest_key},
|
||||
ExpiresIn=EXPIRY,
|
||||
)
|
||||
|
||||
os.unlink(manifest_path)
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"manifest_key": manifest_key,
|
||||
"manifest_url": manifest_url,
|
||||
}
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
result = asyncio.run(_run())
|
||||
return {"statusCode": 200, "body": json.dumps(result)}
|
||||
159
docs/lambdas-md/lambda-lifecycle.svg
Normal file
159
docs/lambdas-md/lambda-lifecycle.svg
Normal file
@@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: lifecycle Pages: 1 -->
|
||||
<svg width="1215pt" height="546pt"
|
||||
viewBox="0.00 0.00 1215.00 546.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 541.5)">
|
||||
<title>lifecycle</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-541.5 1211.25,-541.5 1211.25,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="603.62" y="-518.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Lambda execution environment lifecycle</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_cold</title>
|
||||
<polygon fill="#0a0e17" stroke="#ff3d00" stroke-dasharray="5,2" points="8,-202.25 8,-502 518,-502 518,-202.25 8,-202.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="263" y="-482.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#ff3d00">Cold start (first invocation on a fresh execution environment)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_invoke</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="855,-8 855,-173 1019,-173 1019,-8 855,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="937" y="-153.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Invocation</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_warm</title>
|
||||
<polygon fill="#0a0e17" stroke="#00c853" stroke-dasharray="5,2" points="526,-215.75 526,-307.75 1061,-307.75 1061,-215.75 526,-215.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="793.5" y="-288.55" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#00c853">Warm reuse (subsequent invocations on the same environment)</text>
|
||||
</g>
|
||||
<!-- download -->
|
||||
<g id="node1" class="node">
|
||||
<title>download</title>
|
||||
<polygon fill="#243056" stroke="#1e2a4a" points="503.12,-466.5 370.88,-466.5 370.88,-430.5 503.12,-430.5 503.12,-466.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-451.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">1. Download code</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-438.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">zip / container layers</text>
|
||||
</g>
|
||||
<!-- bootstrap -->
|
||||
<g id="node2" class="node">
|
||||
<title>bootstrap</title>
|
||||
<polygon fill="#243056" stroke="#1e2a4a" points="505.75,-387.25 368.25,-387.25 368.25,-351.25 505.75,-351.25 505.75,-387.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-372.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">2. Start runtime</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-358.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">bootstrap (python3.x)</text>
|
||||
</g>
|
||||
<!-- download->bootstrap -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>download->bootstrap</title>
|
||||
<path fill="none" stroke="#4a5568" d="M437,-430.36C437,-421.12 437,-409.48 437,-398.91"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="440.5,-399.13 437,-389.13 433.5,-399.13 440.5,-399.13"/>
|
||||
</g>
|
||||
<!-- init -->
|
||||
<g id="node3" class="node">
|
||||
<title>init</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="510.25,-285.75 363.75,-285.75 363.75,-210.25 510.25,-210.25 510.25,-285.75"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-271.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">3. Init phase</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-257.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">run module-level code</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-244.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">import boto3 / aioboto3</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-230.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">build clients</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="437" y="-217.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(billed; capped at 10 s)</text>
|
||||
</g>
|
||||
<!-- bootstrap->init -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>bootstrap->init</title>
|
||||
<path fill="none" stroke="#4a5568" d="M437,-350.91C437,-336.89 437,-316.46 437,-297.56"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="440.5,-297.57 437,-287.57 433.5,-297.57 440.5,-297.57"/>
|
||||
</g>
|
||||
<!-- handler -->
|
||||
<g id="node4" class="node">
|
||||
<title>handler</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="1010.62,-137.5 863.38,-137.5 863.38,-89 1010.62,-89 1010.62,-137.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="937" y="-123.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">handler(event, context)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="937" y="-109.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">your code runs</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="937" y="-96.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(billed)</text>
|
||||
</g>
|
||||
<!-- init->handler -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>init->handler</title>
|
||||
<path fill="none" stroke="#0066ff" d="M503.5,-209.81C509.66,-207.04 515.88,-204.46 522,-202.25 632.54,-162.3 766.53,-137.96 851.78,-125.3"/>
|
||||
<polygon fill="#0066ff" stroke="#0066ff" points="852.08,-128.79 861.47,-123.88 851.06,-121.86 852.08,-128.79"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="605.96" y="-183.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">event arrives</text>
|
||||
</g>
|
||||
<!-- respond -->
|
||||
<g id="node5" class="node">
|
||||
<title>respond</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="996,-52 908,-52 908,-16 996,-16 996,-52"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="952" y="-30.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">return / raise</text>
|
||||
</g>
|
||||
<!-- handler->respond -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>handler->respond</title>
|
||||
<path fill="none" stroke="#4a5568" d="M941.58,-88.65C943.13,-80.69 944.87,-71.72 946.48,-63.42"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="949.88,-64.29 948.35,-53.81 943.01,-62.96 949.88,-64.29"/>
|
||||
</g>
|
||||
<!-- freeze -->
|
||||
<g id="node8" class="node">
|
||||
<title>freeze</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="1193.88,-393.5 1054.12,-393.5 1054.12,-345 1193.88,-345 1193.88,-393.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-379.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">freeze</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-365.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">process paused</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-352.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(after handler returns)</text>
|
||||
</g>
|
||||
<!-- respond->freeze -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>respond->freeze</title>
|
||||
<path fill="none" stroke="#4a5568" d="M996.29,-51.39C1052.72,-74.98 1147.76,-124.41 1188,-202.25 1221.33,-266.72 1205.08,-295.28 1184,-327 1181.66,-330.52 1178.9,-333.83 1175.88,-336.92"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1173.59,-334.27 1168.5,-343.56 1178.27,-339.48 1173.59,-334.27"/>
|
||||
</g>
|
||||
<!-- thaw -->
|
||||
<g id="node6" class="node">
|
||||
<title>thaw</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="1050.38,-266 949.62,-266 949.62,-230 1050.38,-230 1050.38,-266"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1000" y="-251.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">thaw</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1000" y="-237.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(microseconds)</text>
|
||||
</g>
|
||||
<!-- thaw->handler -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>thaw->handler</title>
|
||||
<path fill="none" stroke="#00c853" d="M991.76,-229.65C981.97,-209.01 965.41,-174.11 953.05,-148.08"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="956.31,-146.78 948.86,-139.25 949.99,-149.78 956.31,-146.78"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="994.4" y="-183.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">reuse env</text>
|
||||
</g>
|
||||
<!-- reuse -->
|
||||
<g id="node7" class="node">
|
||||
<title>reuse</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="925.62,-272.25 814.38,-272.25 814.38,-223.75 931.62,-223.75 931.62,-266.25 925.62,-272.25"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="925.62,-272.25 925.62,-266.25"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="931.62,-266.25 925.62,-266.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="873" y="-257.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">globals retained:</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="873" y="-244.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">clients, /tmp,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="873" y="-230.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">in-memory caches</text>
|
||||
</g>
|
||||
<!-- reuse->handler -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>reuse->handler</title>
|
||||
<path fill="none" stroke="#00c853" stroke-dasharray="1,5" d="M884.44,-223.27C894.54,-202.31 909.39,-171.53 920.72,-148.01"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="923.81,-149.67 925,-139.14 917.5,-146.63 923.81,-149.67"/>
|
||||
</g>
|
||||
<!-- freeze->thaw -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>freeze->thaw</title>
|
||||
<path fill="none" stroke="#00c853" d="M1092.05,-344.65C1085.01,-339.08 1077.73,-333 1071.25,-327 1053.59,-310.64 1035.31,-290.56 1021.69,-274.86"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="1024.4,-272.65 1015.23,-267.34 1019.09,-277.21 1024.4,-272.65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1095.62" y="-318.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">next event</text>
|
||||
</g>
|
||||
<!-- shutdown -->
|
||||
<g id="node9" class="node">
|
||||
<title>shutdown</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="1179.25,-279 1068.75,-279 1068.75,-217 1179.25,-217 1179.25,-279"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-264.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ff3d00">shutdown</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-251.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ff3d00">idle ~5–15 min →</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-237.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ff3d00">env torn down</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1124" y="-224.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ff3d00">/tmp gone</text>
|
||||
</g>
|
||||
<!-- freeze->shutdown -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>freeze->shutdown</title>
|
||||
<path fill="none" stroke="#ff3d00" stroke-dasharray="5,2" d="M1124,-344.69C1124,-329.32 1124,-308.83 1124,-290.76"/>
|
||||
<polygon fill="#ff3d00" stroke="#ff3d00" points="1127.5,-290.82 1124,-280.82 1120.5,-290.82 1127.5,-290.82"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1151.75" y="-318.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">idle too long</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
193
docs/lambdas-md/lambda-system_overview.svg
Normal file
193
docs/lambdas-md/lambda-system_overview.svg
Normal file
@@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: system_overview Pages: 1 -->
|
||||
<svg width="1531pt" height="306pt"
|
||||
viewBox="0.00 0.00 1531.00 306.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 301.5)">
|
||||
<title>system_overview</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-301.5 1526.75,-301.5 1526.75,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="761.38" y="-278.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Sample app — Lambda + MinIO sandbox</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_caller</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-95 8,-228 121,-228 121,-95 8,-95"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-208.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Caller</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_lambda</title>
|
||||
<polygon fill="#0a0e17" stroke="#0066ff" stroke-dasharray="5,2" points="166.5,-108 166.5,-262 1308,-262 1308,-108 166.5,-108"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="737.25" y="-242.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">Lambda execution environment</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_async</title>
|
||||
<polygon fill="#0a0e17" stroke="#0066ff" stroke-dasharray="1,5" points="409,-116 409,-226 1040.75,-226 1040.75,-116 409,-116"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="724.88" y="-206.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">asyncio.Queue producer / consumer</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_storage</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="1162,-8 1162,-100 1514.75,-100 1514.75,-8 1162,-8"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1338.38" y="-80.8" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Object storage</text>
|
||||
</g>
|
||||
<!-- invoke -->
|
||||
<g id="node1" class="node">
|
||||
<title>invoke</title>
|
||||
<polygon fill="#243056" stroke="#1e2a4a" points="113,-192.5 16,-192.5 16,-103.5 113,-103.5 113,-192.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-178.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">invoke.py</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-164.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(local) /</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-151.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">API Gateway,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-137.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">S3 event,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-124.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">Step Functions</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="64.5" y="-110.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(real AWS)</text>
|
||||
</g>
|
||||
<!-- handler -->
|
||||
<g id="node2" class="node">
|
||||
<title>handler</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="321.75,-166 174.5,-166 174.5,-130 321.75,-130 321.75,-166"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="248.12" y="-151.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">handler(event, context)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="248.12" y="-137.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">lambda_function.py</text>
|
||||
</g>
|
||||
<!-- invoke->handler -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>invoke->handler</title>
|
||||
<path fill="none" stroke="#4a5568" d="M113.22,-148C128.48,-148 145.85,-148 162.92,-148"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="162.54,-151.5 172.54,-148 162.54,-144.5 162.54,-151.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="143.75" y="-150.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">event</text>
|
||||
</g>
|
||||
<!-- producer -->
|
||||
<g id="node3" class="node">
|
||||
<title>producer</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="578.5,-172.25 417,-172.25 417,-123.75 578.5,-123.75 578.5,-172.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-157.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">producer</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-144.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">list_objects_v2 (paginator)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-130.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">filter *.pdf</text>
|
||||
</g>
|
||||
<!-- handler->producer -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>handler->producer</title>
|
||||
<path fill="none" stroke="#4a5568" d="M322.08,-148C348.16,-148 377.89,-148 405.33,-148"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="405.01,-151.5 415.01,-148 405.01,-144.5 405.01,-151.5"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="365.25" y="-150.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">spawn task</text>
|
||||
</g>
|
||||
<!-- consumer -->
|
||||
<g id="node5" class="node">
|
||||
<title>consumer</title>
|
||||
<polygon fill="#0d1a33" stroke="#1e2a4a" points="1032.75,-181.25 888.5,-181.25 888.5,-132.75 1032.75,-132.75 1032.75,-181.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="960.62" y="-166.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">consumer</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="960.62" y="-153.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">generate_presigned_url</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="960.62" y="-139.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">append JSONL</text>
|
||||
</g>
|
||||
<!-- handler->consumer -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>handler->consumer</title>
|
||||
<path fill="none" stroke="#4a5568" d="M321.87,-165.21C349,-171.06 380.15,-177.09 408.75,-181 569.01,-202.91 611.48,-216.8 772.25,-199 807.08,-195.14 844.9,-187.37 877.38,-179.55"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="877.83,-183.05 886.71,-177.27 876.16,-176.25 877.83,-183.05"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="630.25" y="-209.83" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">spawn task</text>
|
||||
</g>
|
||||
<!-- response -->
|
||||
<g id="node9" class="node">
|
||||
<title>response</title>
|
||||
<polygon fill="#243056" stroke="#1e2a4a" points="580.75,-100 408.75,-100 408.75,-38 586.75,-38 586.75,-94 580.75,-100"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="580.75,-100 580.75,-94"/>
|
||||
<polyline fill="none" stroke="#1e2a4a" points="586.75,-94 580.75,-94"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-85.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">response</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-72.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">{count, manifest_key,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-58.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">manifest_url}</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="497.75" y="-45.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">(< 1 KB; sidesteps 6 MB cap)</text>
|
||||
</g>
|
||||
<!-- handler->response -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>handler->response</title>
|
||||
<path fill="none" stroke="#4a5568" d="M306.87,-129.58C333.84,-120.97 366.73,-110.48 397.51,-100.66"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="398.4,-104.05 406.86,-97.68 396.27,-97.38 398.4,-104.05"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="365.25" y="-120.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">return</text>
|
||||
</g>
|
||||
<!-- queue -->
|
||||
<g id="node4" class="node">
|
||||
<title>queue</title>
|
||||
<path fill="#121829" stroke="#1e2a4a" d="M772.25,-184.28C772.25,-187.63 750.18,-190.34 723,-190.34 695.82,-190.34 673.75,-187.63 673.75,-184.28 673.75,-184.28 673.75,-129.72 673.75,-129.72 673.75,-126.37 695.82,-123.66 723,-123.66 750.18,-123.66 772.25,-126.37 772.25,-129.72 772.25,-129.72 772.25,-184.28 772.25,-184.28"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M772.25,-184.28C772.25,-180.94 750.18,-178.22 723,-178.22 695.82,-178.22 673.75,-180.94 673.75,-184.28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="723" y="-166.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">asyncio.Queue</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="723" y="-153.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">maxsize=2000</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="723" y="-139.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(backpressure)</text>
|
||||
</g>
|
||||
<!-- producer->queue -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>producer->queue</title>
|
||||
<path fill="none" stroke="#0066ff" d="M578.66,-148.22C603.39,-148.6 630.71,-149.35 655.75,-150.75 657.91,-150.87 660.11,-151.01 662.34,-151.16"/>
|
||||
<polygon fill="#0066ff" stroke="#0066ff" points="661.8,-154.63 672.04,-151.88 662.33,-147.65 661.8,-154.63"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="630.25" y="-153.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">key</text>
|
||||
</g>
|
||||
<!-- minio -->
|
||||
<g id="node7" class="node">
|
||||
<title>minio</title>
|
||||
<path fill="#1a3a1a" stroke="#1e2a4a" d="M1255.75,-59.69C1255.75,-62.1 1236.53,-64.06 1212.88,-64.06 1189.22,-64.06 1170,-62.1 1170,-59.69 1170,-59.69 1170,-20.31 1170,-20.31 1170,-17.9 1189.22,-15.94 1212.88,-15.94 1236.53,-15.94 1255.75,-17.9 1255.75,-20.31 1255.75,-20.31 1255.75,-59.69 1255.75,-59.69"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M1255.75,-59.69C1255.75,-57.27 1236.53,-55.31 1212.88,-55.31 1189.22,-55.31 1170,-57.27 1170,-59.69"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-43.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">MinIO (local)</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-29.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">or real S3</text>
|
||||
</g>
|
||||
<!-- producer->minio -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>producer->minio</title>
|
||||
<path fill="none" stroke="#4a5568" d="M578.76,-132.08C608.53,-126.37 642.63,-120.1 673.75,-115 721.03,-107.25 1032.17,-64.57 1158.6,-47.28"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1158.95,-50.76 1168.39,-45.94 1158.01,-43.83 1158.95,-50.76"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="830.38" y="-100.82" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">LIST</text>
|
||||
</g>
|
||||
<!-- queue->consumer -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>queue->consumer</title>
|
||||
<path fill="none" stroke="#4a5568" d="M772.48,-155.98C778.47,-155.89 784.5,-155.8 790.25,-155.75 825.92,-155.41 834.83,-155.5 870.5,-155.75 872.56,-155.76 874.64,-155.78 876.74,-155.8"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="876.57,-159.3 886.6,-155.89 876.64,-152.3 876.57,-159.3"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="830.38" y="-158.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">key</text>
|
||||
</g>
|
||||
<!-- tmp -->
|
||||
<g id="node6" class="node">
|
||||
<title>tmp</title>
|
||||
<path fill="#121829" stroke="#1e2a4a" d="M1300,-180.28C1300,-183.63 1260.95,-186.34 1212.88,-186.34 1164.8,-186.34 1125.75,-183.63 1125.75,-180.28 1125.75,-180.28 1125.75,-125.72 1125.75,-125.72 1125.75,-122.37 1164.8,-119.66 1212.88,-119.66 1260.95,-119.66 1300,-122.37 1300,-125.72 1300,-125.72 1300,-180.28 1300,-180.28"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M1300,-180.28C1300,-176.94 1260.95,-174.22 1212.88,-174.22 1164.8,-174.22 1125.75,-176.94 1125.75,-180.28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-162.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">/tmp/<uuid>.jsonl</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-149.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">streamed manifest</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1212.88" y="-135.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(ephemeral, 512 MB default)</text>
|
||||
</g>
|
||||
<!-- consumer->tmp -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>consumer->tmp</title>
|
||||
<path fill="none" stroke="#4a5568" d="M1033,-155.86C1058.16,-155.46 1086.89,-155 1113.86,-154.57"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1113.84,-158.07 1123.78,-154.41 1113.73,-151.07 1113.84,-158.07"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1079.25" y="-158.18" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">JSONL line</text>
|
||||
</g>
|
||||
<!-- consumer->minio -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>consumer->minio</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="1,5" d="M992.03,-132.46C1008.5,-120.4 1029.72,-106.73 1050.75,-98.5 1074.67,-89.14 1083.23,-96.65 1107.75,-89 1125.6,-83.43 1144.31,-75.39 1160.87,-67.42"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1162.09,-70.72 1169.52,-63.16 1159,-64.44 1162.09,-70.72"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1079.25" y="-112.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">presign</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1079.25" y="-101.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">(local HMAC)</text>
|
||||
</g>
|
||||
<!-- tmp->minio -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>tmp->minio</title>
|
||||
<path fill="none" stroke="#4a5568" d="M1212.88,-119.53C1212.88,-105.74 1212.88,-89.75 1212.88,-75.73"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="1216.38,-76.03 1212.88,-66.03 1209.38,-76.03 1216.38,-76.03"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1198.62" y="-94.56" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">put_object</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1198.62" y="-83.31" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">manifests/<uuid>.jsonl</text>
|
||||
</g>
|
||||
<!-- minio->producer -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>minio->producer</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="5,2" d="M1169.68,-40.78C1093.42,-42.74 927.8,-49.54 790.25,-72.75 738.1,-81.55 725.32,-85.28 673.75,-97 642.95,-104 635.14,-105.39 604.75,-114 598.1,-115.88 591.24,-117.9 584.35,-119.99"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="583.53,-116.58 574.99,-122.86 585.58,-123.27 583.53,-116.58"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="830.38" y="-75.45" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">page (1000 keys)</text>
|
||||
</g>
|
||||
<!-- bucket -->
|
||||
<g id="node8" class="node">
|
||||
<title>bucket</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="1506.75,-64.25 1503.75,-68.25 1482.75,-68.25 1479.75,-64.25 1337,-64.25 1337,-15.75 1506.75,-15.75 1506.75,-64.25"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1421.88" y="-49.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">my-company-reports-bucket</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1421.88" y="-36.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">2026/04/*.pdf</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="1421.88" y="-22.8" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">manifests/<uuid>.jsonl</text>
|
||||
</g>
|
||||
<!-- minio->bucket -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
1334
docs/lambdas-md/lambda_study_script.md
Normal file
1334
docs/lambdas-md/lambda_study_script.md
Normal file
File diff suppressed because it is too large
Load Diff
101
docs/viewer.html
Normal file
101
docs/viewer.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Graph Viewer</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: #0a0e17;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
#container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
}
|
||||
#container.dragging { cursor: grabbing; }
|
||||
img {
|
||||
transform-origin: 0 0;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<img id="img" />
|
||||
</div>
|
||||
<script>
|
||||
var src = new URLSearchParams(location.search).get('src');
|
||||
var img = document.getElementById('img');
|
||||
var container = document.getElementById('container');
|
||||
|
||||
img.src = src;
|
||||
|
||||
var scale = 1;
|
||||
var x = 0, y = 0;
|
||||
var dragging = false;
|
||||
var startX, startY, startPanX, startPanY;
|
||||
|
||||
function apply() {
|
||||
img.style.transform = 'translate(' + x + 'px,' + y + 'px) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
// Fit to screen on load
|
||||
img.onload = function() {
|
||||
var sw = window.innerWidth / img.naturalWidth;
|
||||
var sh = window.innerHeight / img.naturalHeight;
|
||||
scale = Math.min(sw, sh) * 0.95;
|
||||
x = (window.innerWidth - img.naturalWidth * scale) / 2;
|
||||
y = (window.innerHeight - img.naturalHeight * scale) / 2;
|
||||
apply();
|
||||
};
|
||||
|
||||
// Wheel zoom toward cursor
|
||||
container.addEventListener('wheel', function(e) {
|
||||
e.preventDefault();
|
||||
var factor = e.deltaY < 0 ? 1.12 : 0.89;
|
||||
var rect = container.getBoundingClientRect();
|
||||
var mx = e.clientX - rect.left;
|
||||
var my = e.clientY - rect.top;
|
||||
x = mx - (mx - x) * factor;
|
||||
y = my - (my - y) * factor;
|
||||
scale *= factor;
|
||||
apply();
|
||||
}, { passive: false });
|
||||
|
||||
// Pan
|
||||
container.addEventListener('mousedown', function(e) {
|
||||
if (e.button !== 0) return;
|
||||
dragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
startPanX = x;
|
||||
startPanY = y;
|
||||
container.classList.add('dragging');
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', function(e) {
|
||||
if (!dragging) return;
|
||||
x = startPanX + (e.clientX - startX);
|
||||
y = startPanY + (e.clientY - startY);
|
||||
apply();
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', function() {
|
||||
dragging = false;
|
||||
container.classList.remove('dragging');
|
||||
});
|
||||
|
||||
// Double-click to reset
|
||||
container.addEventListener('dblclick', function() {
|
||||
img.onload();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
15
invoke.py
Normal file
15
invoke.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
os.environ.setdefault("BUCKET_NAME", "my-company-reports-bucket")
|
||||
os.environ.setdefault("PREFIX", "2026/04/")
|
||||
os.environ.setdefault("S3_ENDPOINT_URL", "http://localhost:9000")
|
||||
os.environ.setdefault("AWS_ACCESS_KEY_ID", "minioadmin")
|
||||
os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "minioadmin")
|
||||
os.environ.setdefault("AWS_REGION", "us-east-1")
|
||||
|
||||
from lambda_function import handler # noqa: E402
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = handler({}, None)
|
||||
print(json.dumps(response, indent=2))
|
||||
79
lambda_function.py
Normal file
79
lambda_function.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aioboto3
|
||||
import aiofiles
|
||||
|
||||
BUCKET = os.environ.get("BUCKET_NAME", "my-company-reports-bucket")
|
||||
PREFIX = os.environ.get("PREFIX", "2026/04/")
|
||||
EXPIRY = int(os.environ.get("URL_EXPIRY_SECONDS", "900"))
|
||||
ENDPOINT = os.environ.get("S3_ENDPOINT_URL") or None
|
||||
QUEUE_MAX = int(os.environ.get("QUEUE_MAX", "2000"))
|
||||
|
||||
_DONE = object()
|
||||
|
||||
|
||||
async def _run():
|
||||
session = aioboto3.Session()
|
||||
async with session.client("s3", endpoint_url=ENDPOINT) as s3:
|
||||
queue: asyncio.Queue = asyncio.Queue(maxsize=QUEUE_MAX)
|
||||
manifest_path = f"/tmp/{uuid.uuid4()}.jsonl"
|
||||
|
||||
async def producer():
|
||||
paginator = s3.get_paginator("list_objects_v2")
|
||||
async for page in paginator.paginate(Bucket=BUCKET, Prefix=PREFIX):
|
||||
for obj in page.get("Contents", []) or []:
|
||||
key = obj["Key"]
|
||||
if key.lower().endswith(".pdf"):
|
||||
await queue.put(key)
|
||||
await queue.put(_DONE)
|
||||
|
||||
async def consumer():
|
||||
count = 0
|
||||
async with aiofiles.open(manifest_path, "w") as f:
|
||||
while True:
|
||||
item = await queue.get()
|
||||
if item is _DONE:
|
||||
break
|
||||
url = await s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": BUCKET, "Key": item},
|
||||
ExpiresIn=EXPIRY,
|
||||
)
|
||||
await f.write(json.dumps({"key": item, "url": url}) + "\n")
|
||||
count += 1
|
||||
return count
|
||||
|
||||
prod_task = asyncio.create_task(producer())
|
||||
count = await consumer()
|
||||
await prod_task
|
||||
|
||||
manifest_key = f"manifests/{uuid.uuid4()}.jsonl"
|
||||
async with aiofiles.open(manifest_path, "rb") as f:
|
||||
body = await f.read()
|
||||
await s3.put_object(
|
||||
Bucket=BUCKET,
|
||||
Key=manifest_key,
|
||||
Body=body,
|
||||
ContentType="application/x-ndjson",
|
||||
)
|
||||
manifest_url = await s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": BUCKET, "Key": manifest_key},
|
||||
ExpiresIn=EXPIRY,
|
||||
)
|
||||
|
||||
os.unlink(manifest_path)
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"manifest_key": manifest_key,
|
||||
"manifest_url": manifest_url,
|
||||
}
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
result = asyncio.run(_run())
|
||||
return {"statusCode": 200, "body": json.dumps(result)}
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
aioboto3>=15.0
|
||||
aiofiles>=23.2
|
||||
boto3>=1.40
|
||||
76
seed.py
Normal file
76
seed.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
BUCKET = os.environ.get("BUCKET_NAME", "my-company-reports-bucket")
|
||||
PREFIX = os.environ.get("PREFIX", "2026/04/")
|
||||
ENDPOINT = os.environ.get("S3_ENDPOINT_URL", "http://localhost:9000")
|
||||
DECOY_EXTS = (".txt", ".csv", ".json")
|
||||
|
||||
|
||||
def _client():
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=ENDPOINT,
|
||||
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID", "minioadmin"),
|
||||
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY", "minioadmin"),
|
||||
region_name=os.environ.get("AWS_REGION", "us-east-1"),
|
||||
config=Config(signature_version="s3v4"),
|
||||
)
|
||||
|
||||
|
||||
def _ensure_bucket(s3, name):
|
||||
try:
|
||||
s3.head_bucket(Bucket=name)
|
||||
except ClientError:
|
||||
s3.create_bucket(Bucket=name)
|
||||
|
||||
|
||||
def _walk(source_dir):
|
||||
for root, _, files in os.walk(source_dir):
|
||||
for name in files:
|
||||
yield os.path.join(root, name)
|
||||
|
||||
|
||||
def main():
|
||||
source_dir = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("SOURCE_DIR")
|
||||
if not source_dir:
|
||||
print("usage: SOURCE_DIR=<path> python seed.py (or pass as argv[1])", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if not os.path.isdir(source_dir):
|
||||
print(f"not a directory: {source_dir}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
s3 = _client()
|
||||
_ensure_bucket(s3, BUCKET)
|
||||
|
||||
pdf_n = decoy_n = 0
|
||||
for path in _walk(source_dir):
|
||||
lower = path.lower()
|
||||
is_pdf = lower.endswith(".pdf")
|
||||
is_decoy = lower.endswith(DECOY_EXTS)
|
||||
if not (is_pdf or is_decoy):
|
||||
continue
|
||||
|
||||
rel = os.path.relpath(path, source_dir).replace(os.sep, "/")
|
||||
key = f"{PREFIX}{rel}"
|
||||
try:
|
||||
s3.upload_file(path, BUCKET, key)
|
||||
except (ClientError, OSError) as exc:
|
||||
print(f" skip {path}: {exc}", file=sys.stderr)
|
||||
continue
|
||||
if is_pdf:
|
||||
pdf_n += 1
|
||||
else:
|
||||
decoy_n += 1
|
||||
if (pdf_n + decoy_n) % 100 == 0:
|
||||
print(f" uploaded {pdf_n} pdfs / {decoy_n} decoys ...")
|
||||
|
||||
print(f"done: {pdf_n} pdfs and {decoy_n} decoys uploaded to s3://{BUCKET}/{PREFIX}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user