Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@
// require intrusive middleware changes beyond this PR's scope.
"minLines": 6,
"ignore": [
// AWS Lambda and GCP Cloud Run deliberately mirror the same distributed
// rendering lifecycle while retaining provider-specific SDK, storage, and
// retry semantics. The Plan v2 AWS adapter extends that existing symmetry;
// extracting a shared cloud abstraction would couple independent packages.
"packages/aws-lambda/src/handler.ts",
"packages/aws-lambda/src/s3Transport.ts",
// sourcePatcher.ts: pre-existing internal clones between the inline-style
// and attribute tag-patchers; only the PatchOperation type gained two
// optional fields here, but the line shift makes fallow re-flag them.
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 26 additions & 5 deletions examples/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ aws stepfunctions start-execution \
"ProjectS3Uri": "s3://${RENDER_BUCKET}/projects/my-project.tar.gz",
"PlanOutputS3Prefix": "s3://${RENDER_BUCKET}/renders/$(date +%s)/",
"OutputS3Uri": "s3://${RENDER_BUCKET}/output.mp4",
"PlanProtocol": "v1",
"Config": {
"fps": 30,
"width": 1920,
Expand All @@ -91,6 +92,9 @@ EOF

The Step Functions execution kicks off Plan, fans out RenderChunk via
the Map state, and finally Assemble. Final mp4 lands at `OutputS3Uri`.
`PlanProtocol` may be `"v1"` or `"v2"`; absent defaults to v1. V2 uses
separate manifest and content-addressed artifact locators throughout the
workflow and never places a v2 object in `PlanS3Uri`.

## Local invocation

Expand Down Expand Up @@ -119,13 +123,14 @@ the architecture works on a deployed Lambda — use the local smoke
script:

```bash
# All defaults (mp4-h264-sdr fixture, chunk counts 2/4/8, PSNR >= 40 dB).
# Defaults use the fixture's meta.json minPsnr (30 dB for mp4-h264-sdr).
./scripts/smoke.sh

# Customised:
./scripts/smoke.sh \
--fixture mp4-h264-sdr \
--chunk-counts 2,4,8,16 \
--plan-protocol both \
--psnr-threshold 40 \
--reserved-concurrency 8

Expand All @@ -141,7 +146,21 @@ per-run stack name, renders the fixture at each chunk count via the
Step Functions state machine, PSNR-compares against the in-process
baseline (which is git-LFS tracked under
`packages/producer/tests/distributed/<fixture>/output/`), captures
per-execution Step Functions history, and tears the stack down.
per-execution Step Functions history, and tears the stack down. Use
`--plan-protocol both` to run v1 and v2 through the same deployed Lambda
package and baseline. Each v1/v2 pair is also gated directly on per-chunk
hashes from Step Functions history, normalized decoded RGBA frame hashes,
decoded 48 kHz stereo s16le PCM hashes and byte counts, normalized stream
metadata, and duration. Encoded MP4 SHA equality is reported but is
informational unless `--require-encoded-sha-equal` is set. The script
assigns unique function/state-machine names, uses a
dedicated temporary SAM artifact bucket, and removes render objects,
retained buckets, the implicit Lambda log group, and deployment artifacts
on teardown. Suspended-version buckets are purged in 1,000-entry batches,
including concrete versions, null versions, and delete markers. It then
verifies that the stack, both buckets, Lambda, state-machine, and both log
groups are absent; an otherwise-successful run fails if cleanup cannot be
proven.

**Wall-clock methodology caveat (`eval.sh` only).** `eval.sh` reports a
local-vs-Lambda "speedup" column. The local timing includes `bun` +
Expand All @@ -162,9 +181,11 @@ spend is roughly $0.10-$0.20 per pass before S3 transfer. Lower

Outputs land under `<repo-root>/lambda-smoke-artifacts/`:

- `results.json` — `chunkCount × wallClockMs × psnrAvgDb`
- `renders/N<N>-output.mp4` — each rendered chunk count
- `renders/N<N>-history.json` — full Step Functions execution history
- `results.json` — `planProtocol × chunkCount × wallClockMs × psnrAvgDb`
- `semantic-comparisons.json` — direct v1/v2 semantic gate results
- `renders/<protocol>-N<N>-output.mp4` — each rendered variant
- `renders/<protocol>-N<N>-history.json` — full Step Functions execution history
- `renders/v1-v2-N<N>.*` — normalized frame hashes, ffprobe metadata, and comparison JSON

Prerequisites: `aws` (v2), `sam` (≥ 1.100), `bun` (≥ 1.3), `ffmpeg`,
`jq`, `zip`. AWS credentials come from the standard resolution chain
Expand Down
11 changes: 11 additions & 0 deletions examples/aws-lambda/sample-events/assemble-v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Action": "assemble",
"PlanProtocol": "v2",
"PlanV2ManifestS3Uri": "s3://example-bucket/renders/sample/v2/manifest.json",
"PlanV2ArtifactS3Prefix": "s3://example-bucket/renders/sample/v2/artifacts/sha256",
"PlanHash": "0000000000000000000000000000000000000000000000000000000000000000",
"ChunkS3Uris": ["s3://example-bucket/renders/sample/chunks/0000.mp4"],
"AudioS3Uri": null,
"OutputS3Uri": "s3://example-bucket/renders/sample/output.mp4",
"Format": "mp4"
}
15 changes: 15 additions & 0 deletions examples/aws-lambda/sample-events/plan-v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"Action": "plan",
"PlanProtocol": "v2",
"ProjectS3Uri": "s3://example-bucket/projects/sample.tar.gz",
"PlanOutputS3Prefix": "s3://example-bucket/renders/sample/",
"Config": {
"fps": 30,
"width": 1920,
"height": 1080,
"format": "mp4",
"chunkSize": 240,
"maxParallelChunks": 8,
"runtimeCap": "lambda"
}
}
10 changes: 10 additions & 0 deletions examples/aws-lambda/sample-events/render-chunk-v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Action": "renderChunk",
"PlanProtocol": "v2",
"PlanV2ManifestS3Uri": "s3://example-bucket/renders/sample/v2/manifest.json",
"PlanV2ArtifactS3Prefix": "s3://example-bucket/renders/sample/v2/artifacts/sha256",
"PlanHash": "0000000000000000000000000000000000000000000000000000000000000000",
"ChunkIndex": 0,
"ChunkOutputS3Prefix": "s3://example-bucket/renders/sample/",
"Format": "mp4"
}
181 changes: 181 additions & 0 deletions examples/aws-lambda/scripts/_aws-isolation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# AWS resource-name isolation and failed-deploy discovery helpers.

hf_new_smoke_run_id() {
local seconds seed digest
seconds=$(date +%s)
seed="${seconds}:$$:${RANDOM}:${BASHPID:-$$}"
digest=$(printf '%s' "$seed" | sha256sum | awk '{print substr($1,1,16)}')
printf '%s-%s\n' "$seconds" "$digest"
}

hf_sam_deploy_bucket_name() {
local account_id="$1" region="$2" run_id="$3" digest
digest=$(printf '%s' "$run_id" | sha256sum | awk '{print substr($1,1,20)}')
printf 'hf-sam-%s-%s-%s\n' "$account_id" "$region" "$digest"
}

hf_derive_project_name() {
local stack_name="$1" prefix digest
prefix=$(printf '%s' "$stack_name" |
tr -c '[:alnum:]-' '-' |
sed -E 's/^-+//; s/-+$//' |
cut -c1-36)
[ -n "$prefix" ] || prefix="hf-smoke"
digest=$(printf '%s' "$stack_name" | sha256sum | awk '{print substr($1,1,12)}')
printf '%s-%s\n' "$prefix" "$digest"
}

hf_known_absent() {
local pattern="$1" output_file="$2"
grep -Eiq "$pattern" "$output_file"
}

hf_assert_command_absent() {
local label="$1" absent_pattern="$2"
shift 2
local output_file status detail
output_file=$(mktemp)
if "$@" >"$output_file" 2>&1; then
echo "ERROR: destructive-isolation collision: $label already exists" >&2
rm -f "$output_file"
return 1
else
status=$?
fi
if ! hf_known_absent "$absent_pattern" "$output_file"; then
detail=$(tr '\n' ' ' < "$output_file" | cut -c1-240)
echo "ERROR: could not prove $label absent (exit=$status): $detail" >&2
rm -f "$output_file"
return 2
fi
rm -f "$output_file"
}

hf_assert_named_list_absent() {
local label="$1"
shift
local output_file output status detail
output_file=$(mktemp)
if output=$("$@" 2>"$output_file"); then
if [ -n "$output" ]; then
echo "ERROR: destructive-isolation collision: $label already exists ($output)" >&2
rm -f "$output_file"
return 1
fi
else
status=$?
detail=$(tr '\n' ' ' < "$output_file" | cut -c1-240)
echo "ERROR: could not verify $label absence (exit=$status): $detail" >&2
rm -f "$output_file"
return 2
fi
rm -f "$output_file"
}

# Fail closed unless every exact name this smoke run can destructively clean
# is absent. Call before arming cleanup or creating any AWS resource.
hf_assert_deploy_isolation() {
local stack_name="$1" project_name="$2"
local function_name="${project_name}-render"
local lambda_log="/aws/lambda/${function_name}"
local states_log="/aws/states/${function_name}"

hf_assert_command_absent "CloudFormation stack $stack_name" "does not exist" \
aws cloudformation describe-stacks --stack-name "$stack_name" &&
hf_assert_command_absent "Lambda function $function_name" \
"ResourceNotFoundException|Function not found" \
aws lambda get-function --function-name "$function_name" &&
hf_assert_named_list_absent "Step Functions state machine $function_name" \
aws stepfunctions list-state-machines \
--query "stateMachines[?name=='$function_name'].stateMachineArn" --output text &&
hf_assert_named_list_absent "log group $lambda_log" \
aws logs describe-log-groups --log-group-name-prefix "$lambda_log" \
--query "logGroups[?logGroupName=='$lambda_log'].logGroupName" --output text &&
hf_assert_named_list_absent "log group $states_log" \
aws logs describe-log-groups --log-group-name-prefix "$states_log" \
--query "logGroups[?logGroupName=='$states_log'].logGroupName" --output text
}

# Atomically reserve the exact stack name before SAM can create or update it.
# CloudFormation's create-stack call is the compare-and-set: only one concurrent
# smoke run can acquire a name that both preflight checks observed as absent.
hf_reserve_smoke_stack() {
local stack_name="$1" run_id="$2"
aws cloudformation create-stack \
--stack-name "$stack_name" \
--template-body \
'{"Resources":{"SmokeOwnershipHandle":{"Type":"AWS::CloudFormation::WaitConditionHandle"}}}' \
--tags "Key=HyperframesSmokeRun,Value=$run_id" >/dev/null &&
aws cloudformation wait stack-create-complete --stack-name "$stack_name"
}

# Print "owned" when the stack has this run's ownership tag and
# "absent" when there is no stack. Any foreign/missing tag or AWS API error
# fails closed so a cleanup trap cannot delete a concurrent run's resources.
hf_stack_ownership_status() {
local stack_name="$1" run_id="$2" output_file error_file owner status detail
output_file=$(mktemp)
error_file=$(mktemp)
if aws cloudformation describe-stacks \
--stack-name "$stack_name" \
--query "Stacks[0].Tags[?Key=='HyperframesSmokeRun'].Value | [0]" \
--output text >"$output_file" 2>"$error_file"; then
owner=$(tr -d '\r\n' <"$output_file")
rm -f "$output_file" "$error_file"
if [ "$owner" != "$run_id" ]; then
echo "ERROR: refusing cleanup: stack ownership is '${owner:-missing}', expected '$run_id'" >&2
return 3
fi
printf 'owned\n'
return
else
status=$?
fi
if hf_known_absent "does not exist" "$error_file"; then
rm -f "$output_file" "$error_file"
printf 'absent\n'
return
fi
detail=$(tr '\n' ' ' <"$error_file" | cut -c1-240)
echo "ERROR: could not verify stack ownership (exit=$status): $detail" >&2
rm -f "$output_file" "$error_file"
return 2
}

# Return a JSON object with any physical resources CloudFormation managed to
# create, even when stack outputs were never populated. A genuinely absent
# stack is an empty result; auth/network/query failures are errors.
hf_discover_stack_resources() {
local stack_name="$1" output_file error_file status detail
output_file=$(mktemp)
error_file=$(mktemp)
if aws cloudformation list-stack-resources \
--stack-name "$stack_name" --output json >"$output_file" 2>"$error_file"; then
jq '{
renderBucket: (
[.StackResourceSummaries[]?
| select(.LogicalResourceId == "RenderBucket")
| .PhysicalResourceId][0] // ""
),
stateMachineArn: (
[.StackResourceSummaries[]?
| select(.LogicalResourceId == "RenderStateMachine")
| .PhysicalResourceId][0] // ""
)
}' "$output_file"
rm -f "$output_file" "$error_file"
return
else
status=$?
fi
if hf_known_absent "does not exist" "$error_file"; then
printf '{"renderBucket":"","stateMachineArn":""}\n'
rm -f "$output_file" "$error_file"
return
fi
detail=$(tr '\n' ' ' < "$error_file" | cut -c1-240)
echo "ERROR: failed to discover physical stack resources (exit=$status): $detail" >&2
rm -f "$output_file" "$error_file"
return 2
}
77 changes: 77 additions & 0 deletions examples/aws-lambda/scripts/_s3-purge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# S3 bucket cleanup helpers. Sourcing this file has no side effects.

# Delete every concrete object version and delete marker from a bucket.
#
# We intentionally re-list the first 1,000 entries after every delete batch
# instead of advancing markers through a mutating result set. This handles
# arbitrary pagination depth while avoiding skipped keys when the page being
# used as a cursor has just been removed. It is also required for buckets with
# versioning Suspended: `aws s3 rm` only creates null-version delete markers
# and leaves the historical/null versions behind.
hf_purge_s3_bucket_versions() {
local bucket="$1" work page delete_request delete_response count errors rounds=0
work=$(mktemp -d)
page="$work/page.json"
delete_request="$work/delete.json"
delete_response="$work/delete-response.json"

while true; do
rounds=$((rounds + 1))
if [ "$rounds" -gt 100000 ]; then
echo "ERROR: S3 purge exceeded 100000 batches for s3://$bucket" >&2
rm -rf "$work"
return 1
fi

if ! aws s3api list-object-versions \
--bucket "$bucket" \
--max-keys 1000 \
--no-paginate \
--output json > "$page"; then
echo "ERROR: failed to list object versions for s3://$bucket" >&2
rm -rf "$work"
return 1
fi

jq '{
Objects: [
(.Versions // [])[],
(.DeleteMarkers // [])[]
] | map({Key, VersionId}),
Quiet: true
}' "$page" > "$delete_request"
count=$(jq '.Objects | length' "$delete_request")
if [ "$count" -eq 0 ]; then
break
fi

if ! aws s3api delete-objects \
--bucket "$bucket" \
--delete "file://$delete_request" \
--output json > "$delete_response"; then
echo "ERROR: failed to delete a version batch from s3://$bucket" >&2
rm -rf "$work"
return 1
fi
# Successful Quiet=true deletes may produce a zero-byte response body.
# Slurp mode treats that as an empty input set and therefore zero errors,
# while still counting per-object Errors when AWS returns a JSON object.
errors=$(jq -s '[.[] | (.Errors // [])[]] | length' "$delete_response")
if [ "$errors" -ne 0 ]; then
echo "ERROR: S3 returned per-object deletion errors for s3://$bucket:" >&2
jq -c '.Errors[]' "$delete_response" >&2
rm -rf "$work"
return 1
fi
echo " purged $count object versions/delete markers from s3://$bucket"
done

rm -rf "$work"
}

hf_delete_s3_bucket_completely() {
local bucket="$1"
hf_purge_s3_bucket_versions "$bucket" &&
aws s3api delete-bucket --bucket "$bucket"
}
Loading
Loading