Skip to content

Fix EFS mount failure by condensing generated volume names - #42

Closed
rpmcginty wants to merge 1 commit into
mainfrom
fix/efs-volume-name-length
Closed

rpmcginty wants to merge 1 commit into
mainfrom
fix/efs-volume-name-length

Conversation

@rpmcginty

Copy link
Copy Markdown
Collaborator

Demand execution batch jobs fail at container start, before any user code runs:

CannotStartContainerError: ... VolumeDriver.Mount: failed to mount volume
ecs-dev-de-<execution_type>-<64 hex>-1-dev-de-core-opt-fsap-0acb9f234e1b57786-scratch-vol-<20 hex>:
mounting volume failed: Failed to create self-signed client-side certificate ...
Write out database with 1 new entries
File name too long

Root cause

Not a NAME_MAX / PATH_MAX limit — OpenSSL's hard-coded 256-byte stack buffer. The EFS volume name flows through a chain of generated names:

batch job definition name  ->  ECS task family
  -> docker volume name        ecs-<family>-<revision>-<volume>-<20 hex>
  -> efs-utils TLS state dir   <fs-id>.var.lib.ecs.volumes.<volume>.<port>+
  -> openssl CA database       /var/run/efs/<state dir>/database/index.txt

openssl ca guards that path with if (j + 6 >= BSIZE) where BSIZE is 256 (apps/lib/apps.c), so the database path must be ≤ 246 chars. Working back up the chain:

limit
CA database path ≤ 246
efs-utils state dir ≤ 214
ECS docker volume name ≤ 166
task family + revision + volume name ≤ 139

We were at 143 — over by exactly 4. The /database/ subdirectory is the easy piece to miss: config.conf sits at 243 and writes fine, which is why the log shows the config being read and the certificate signed before it dies, and why serial (247, guarded by j + 1) rotated successfully while index.txt did not.

Nothing fails at registration. It fails minutes later, in a message that names none of the code that chose the name.

Changes

New common/naming.py:

  • condense_str(value, max_length, delimiter, hash_length) — the general primitive. Shortens a value to a budget, keeping as much readable prefix as possible and appending delimiter + a hash of the full original value, so two values sharing a long prefix still condense differently. A value that already fits is returned unchanged, so adopting this renames nothing. Deterministic, because an unstable name would register a new job definition revision on every run. Raises rather than silently truncating when the budget cannot fit the suffix plus a prefix character.
  • build_efs_volume_name(...) — leads with the mount path basename (scratch/shared/tmp) so volumes stay identifiable, and takes uniqueness from a hash over (file system, access point, mount path) instead of spelling all three out. 50 → 16 chars.
  • check_ecs_volume_component_budget(...) — raises while the job definition is being built, so an over-long name is an immediate legible error instead of a CannotStartContainerError. Reserves 3 revision digits, so a name cannot pass at revision 9 and fail at revision 10.

Why not just cap execution_type

Capping it at 8 chars also clears the budget and needs no deploy — it's what unblocked testing today. But it pushes an OpenSSL buffer size onto callers, and a descriptive execution type shouldn't be able to break EFS mounting. Its hard cap would be 16 chars, and 16 lands on exactly zero margin.

execution_type old new
ocsdv452-filter-test (the failure) 143/139 ✗ 109/139 ✓
a-fairly-descriptive-execution-type-name (40) 163/139 ✗ 129/139 ✓

Testing

make format && make lint && make test — 177 passed, ruff and mypy clean. 25 new tests covering the no-op-when-it-fits property, determinism, prefix-collision resistance, budget validation, ECS charset safety, and uniqueness across access points and mount paths.

Two existing tests asserted the old volume name format and were updated. test_context_manager now asserts structure (the ids are moto-generated, so the hash can't be hardcoded) plus the invariant that the mount point's sourceVolume matches the registered volume name; test_scaffolding pins the new literal, since its ids are fixed.

Notes

  • Independent of the OCSDV-452/453 release train. Branched from main and passing against published core/aws-utils, so it can merge without waiting for R1/R2.
  • Existing job definitions are unaffected in identity — volume names are not inputs to the execution hash, so this changes containerProperties only. Every existing family registers one new revision on its next run.
  • Prod has less headroom than dev-de — it adds {name}-part{i} EFS ecosystems with longer names. This change helps there too, but recompute before assuming margin.
  • Two alternatives considered and rejected: truncating the 64-char job-definition hash (works, but changes job definition identity for every demand execution everywhere), and disabling transitEncryption (sends all EFS traffic cleartext across the VPC — "fixes" this by deleting the failing code path).

🤖 Generated with Claude Code

Demand execution batch jobs failed at container start with:

  CannotStartContainerError: ... Failed to create self-signed client-side
  certificate ... File name too long

This is not a NAME_MAX/PATH_MAX limit. The EFS volume name flows through a
chain of generated names and lands in an OpenSSL buffer:

  batch job definition name -> ECS task family
    -> docker volume name    ecs-<family>-<rev>-<volume>-<20 hex>
    -> efs-utils TLS state dir
    -> openssl CA database   /var/run/efs/<state dir>/database/index.txt

openssl guards that path against a hard-coded 256 byte stack buffer (BSIZE in
apps/lib/apps.c), which caps the database path at 246 chars and, working back
up the chain, caps family + revision + volume name at 139. A volume name of
"dev-de-core-opt-fsap-0acb9f234e1b57786-scratch-vol" (50 chars) alongside a
92 char job definition name came to 143 -- over by 4.

Adds common/naming.py:

  * condense_str(value, max_length, delimiter, hash_length) -- shortens a value
    to a budget, keeping a readable prefix and appending a hash of the FULL
    original so values sharing a prefix stay distinct. A value that already
    fits is returned unchanged, so adopting it renames nothing.
  * build_efs_volume_name(...) -- leads with the mount path basename
    (scratch/shared/tmp) and takes uniqueness from a hash over the file system,
    access point and mount path rather than spelling them all out. 50 -> 16 chars.
  * check_ecs_volume_component_budget(...) -- raises while the job definition is
    being built rather than minutes later at container start, where the error
    names nothing responsible.

Capping execution_type would also have worked, but pushes an OpenSSL buffer
size onto callers; a descriptive execution type should not break EFS mounting.
With this change a 40 char execution_type fits comfortably (129/139) where the
20 char one previously failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.87%. Comparing base (359e824) to head (1afd94d).

Files with missing lines Patch % Lines
src/aibs_informatics_aws_lambda/common/naming.py 93.54% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #42      +/-   ##
==========================================
+ Coverage   90.80%   90.87%   +0.07%     
==========================================
  Files          26       27       +1     
  Lines        1446     1480      +34     
  Branches      137      143       +6     
==========================================
+ Hits         1313     1345      +32     
- Misses         96       97       +1     
- Partials       37       38       +1     
Files with missing lines Coverage Δ
...tics_aws_lambda/handlers/demand/context_manager.py 96.65% <100.00%> (+0.04%) ⬆️
src/aibs_informatics_aws_lambda/common/naming.py 93.54% <93.54%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rpmcginty

Copy link
Copy Markdown
Collaborator Author

Superseded. Relocated per review:

  • condense_str moved to aibs-informatics-core utils.tools.strtools (PR-A branch feature/OCSDV-452-data-sync-filter-models, commit 6ec5e0c) — nothing about it is Batch or EFS specific, and it now ships with the R1 release rather than as a lambda-local helper.
  • The Batch/EFS budget arithmetic moved to aibs-informatics-aws-lambda handlers/demand/naming.py (was common/naming.py), and now rides on PR-E (OCSDV-453 (PR-E): carry per-parameter filters into demand execution data syncs #41) rather than a standalone branch.

Root-cause analysis and the character budget are preserved in the new commit message and the module docstring.

@rpmcginty rpmcginty closed this Aug 12, 2026
@rpmcginty
rpmcginty deleted the fix/efs-volume-name-length branch August 12, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant