Skip to content

EMFILE "too many files open" on Windows due to un‑awaited archiver streams (descriptor leak & uncontrolled concurrency) #839

@joroaf

Description

@joroaf

vropkg: EMFILE "too many files open" on Windows due to un-awaited archiver streams (descriptor leak & uncontrolled concurrency)

Summary

Running vropkg on Windows for packages with many elements fails with:

Error: EMFILE: too many open files

Archive write streams are created and finalized without awaiting their completion. High parallelism (Promise.all across elements) causes many simultaneous open file descriptors until the OS limit is hit (Windows defaults are low). Also introduces a race where signing may read files before archives are flushed.

Affected Code

  • typescript/vropkg/src/serialize/flat.ts
    • initializeContext.bundle returns the result of .finalize() (void) but is typed/used like a Promise<void>.
    • serializeFlatElements launches many async tasks per element concurrently.
  • typescript/vropkg/src/serialize/util.ts
    • zipbundle starts an archiver pipeline and returns immediately; no Promise on stream close.
  • typescript/vropkg/src/packaging.ts
    • archive() creates and pipes an archiver but callers don't wait for the destination stream to finish.

Impact

  • Frequent failure on Windows with medium/large packages (e.g. ≥512 elements).
  • Potential race: signatures may be generated before archive contents are fully written.

Environment (example)

  • OS: Windows 10 / Windows Server 2019
  • Node.js: 18.x / 20.x / 22.x
  • Branch: main (as of 2025-08-27)

Steps to Reproduce

  1. Prepare a project producing a vRO package with 100+ elements (some with bundles/resources).
  2. Run vropkg to build the package.
  3. Observe eventual failure with EMFILE.

Expected

Packaging completes; file descriptors are reused; signatures generated after all archive writes complete.

Actual

Multiple archives in progress simultaneously; write streams not awaited; EMFILE thrown.

Root Cause

archiver.finalize() is not asynchronous (returns void). Code assumes it can be awaited. Without listening to finish/close on the output stream, promises resolve too early (or are not real promises), allowing unbounded concurrency and exhausting file handles.

Proposed Fix

  1. Wrap each archiving operation in a real Promise resolving on output stream close (or finish) and rejecting on error.
  2. Correct return types (ensure functions actually return Promise<void>).
  3. Await archive promises (e.g., await context.save(...)).
  4. Add simple concurrency control for element serialization (e.g., p-limit with a reasonable cap or process elements sequentially).
  5. Ensure signature generation runs only after all archive writes have completed.
  6. Add an integration test packaging a large synthetic set of elements to assert success without EMFILE.

Patch Sketch (conceptual)

// In serialize/util.ts
export const zipbundle = (target: string) => {
  fs.mkdirsSync(target);
  return (file: string) => (sourcePath: string, isDir: boolean): Promise<void> => {
    const absoluteZipPath = path.join(target, file);
    if (!isDir) {
      fs.copySync(sourcePath, absoluteZipPath);
      return Promise.resolve();
    }
    return new Promise((resolve, reject) => {
      const output = fs.createWriteStream(absoluteZipPath);
      const archive = archiver('zip', { zlib: { level: 9 }});
      output.on('close', resolve).on('error', reject);
      archive.on('error', reject);
      archive.directory(sourcePath, false);
      archive.pipe(output);
      archive.finalize();
    });
  };
};

(Apply similar pattern for final package bundle and resource element bundles.)

Workarounds

  • Reduce elements per run (split package).
  • Increase Windows file handle limit (not always feasible).
  • Run on Linux/macOS (higher default limits) — mitigates but doesn’t fix root cause.

Acceptance Criteria

  • Large package builds on Windows without EMFILE.
  • Signatures are created only after archive completion.
  • No regression in output structure.

Additional Notes

Fix is low risk and localized to I/O lifecycle management; may slightly reduce parallel throughput but improves stability and determinism.


Attached Log:

[INFO] info: Using certificate file target/keystore-1.0.4/private_key.pem
[INFO] info: Using certificate file target/keystore-1.0.4/cert.pem
[ERROR] node:fs:562
[ERROR]   return binding.open(
[ERROR]                  ^
[ERROR]
[ERROR] Error: EMFILE: too many open files, open 'C:\Users\txdcy\Documents\projects\di\workflows\target\vropkg\elements\758fd262-5974-497d-9a6a-4a4223697fc4\content-signature'
[ERROR]     at Object.openSync (node:fs:562:18)
[ERROR]     at Object.writeFileSync (node:fs:2440:35)
[ERROR]     at C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\util.js:39:12
[ERROR]     at Generator.next (<anonymous>)
[ERROR]     at C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\util.js:8:71
[ERROR]     at new Promise (<anonymous>)
[ERROR]     at __awaiter (C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\util.js:4:12)
[ERROR]     at Object.contentSignature (C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\util.js:35:51)
[ERROR]     at C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\flat.js:220:20
[ERROR]     at Generator.next (<anonymous>)
[ERROR]     at fulfilled (C:\Users\txdcy\Documents\projects\di\workflows\node_modules\@vmware-pscoe\vropkg\dist\serialize\flat.js:5:58) {
[ERROR]   errno: -4066,
[ERROR]   code: 'EMFILE',
[ERROR]   syscall: 'open',
[ERROR]   path: 'C:\\Users\\txdcy\\Documents\\projects\\di\\workflows\\target\\vropkg\\elements\\758fd262-5974-497d-9a6a-4a4223697fc4\\content-signature'
[ERROR] }
[ERROR]
[ERROR] Node.js v22.16.0
[INFO] Running vropkg... finished

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions