diff --git a/lib/dev/build_container.rb b/lib/dev/build_container.rb index c099714..d6f344a 100644 --- a/lib/dev/build_container.rb +++ b/lib/dev/build_container.rb @@ -44,6 +44,20 @@ class LocalMountsUnsupportedError < RuntimeError; end CONTENT_FILES = ["Dockerfile", ".dockerignore", "deps.lock", "build-deps.lock"].freeze TAG_PREFIX = "content-" + # Rosetta-for-Linux forwards cross-thread signals through rt_tgsigqueueinfo + # and aborts the whole process when the kernel answers EAGAIN ("rosetta + # error: rt_tgsigqueueinfo failed in pend_signal: 11") — no backoff, because + # retrying inside its own signal-forwarding path risks deadlock. The queue + # fills because .NET GC suspensions across UBT/UHT's hundreds of threads + # each become a queued signal under emulation, and RLIMIT_SIGPENDING is one + # per-UID bucket shared by every build process (RAM-scaled default, ~96k on + # a 24GB VM). The bursts are transient — the queue drains in milliseconds + # once a suspension completes — so a wide bucket absorbs them. The limit is + # a cap, not a reservation: ~80 bytes of kernel memory per actually-queued + # signal, worst case ~80MB, nothing at rest. Applied to every build/prewarm + # run; harmless on native hosts. + SIGPENDING_ULIMIT = ["--ulimit", "sigpending=1000000"].freeze + # @return [Dev::ContainerEngine] the engine every docker invocation rides sig { returns(Dev::ContainerEngine) } attr_reader :engine @@ -393,6 +407,7 @@ def docker_run_command(image_tag, project_root:, shell_cmd:, volumes: [], env: { [ *@engine.argv_prefix, "run", "--rm", + *SIGPENDING_ULIMIT, "-v", "#{project_root}:/project", *volume_flags(volumes), *env_flags, @@ -510,6 +525,7 @@ def prewarm_commit!(base_tag, final_tag, volumes:, prewarm:, secrets:) run_argv = [ *@engine.argv_prefix, "run", "--name", container, + *SIGPENDING_ULIMIT, *volume_flags(volumes), *secret_mounts, base_tag, diff --git a/test/dev/build_container_test.rb b/test/dev/build_container_test.rb index 7bb9a4c..8d48122 100644 --- a/test/dev/build_container_test.rb +++ b/test/dev/build_container_test.rb @@ -113,6 +113,19 @@ def build_container(engine: FakeContainerEngine.new) cmd.last(3) == ["sh", "-c", "./bin/build.sh"] end + test "docker_run_command widens the pending-signal ulimit (Rosetta pend_signal aborts)" do + When "building a docker run command" + cmd = build_container.docker_run_command( + "jpduchesne89/snappy:content-abc123", + project_root: Pathname("/project"), + shell_cmd: "./bin/build.sh", + ) + + Then "the run carries the widened sigpending bucket" + cmd.include?("sigpending=1000000") + cmd[cmd.index("sigpending=1000000") - 1] == "--ulimit" + end + test "docker_run_command renders extra volume mounts" do When "building a docker run command with volumes" cmd = build_container.docker_run_command( @@ -1034,6 +1047,7 @@ def build_container(engine: FakeContainerEngine.new) 1 * bc.prewarm_container_name >> "dev-prewarm-test" 1 * bc.write_secret_files({ "WWISE_TOKEN" => "tok" }) >> { "WWISE_TOKEN" => "/tmp/dev-secret-xyz" } 1 * bc.run_watched(["docker", "run", "--name", "dev-prewarm-test", + "--ulimit", "sigpending=1000000", "-v", "/engines/ue:/ue", "-v", "/tmp/dev-secret-xyz:/run/secrets/WWISE_TOKEN:ro", "img:tag-base", "sh", "-c", "bash /work/bin/prewarm.sh"], container: "dev-prewarm-test") >> true @@ -1053,6 +1067,7 @@ def build_container(engine: FakeContainerEngine.new) 1 * bc.prewarm_container_name >> "dev-prewarm-test" 1 * bc.write_secret_files({}) >> {} 1 * bc.run_watched(["docker", "run", "--name", "dev-prewarm-test", + "--ulimit", "sigpending=1000000", "img:tag-base", "sh", "-c", "false"], container: "dev-prewarm-test") >> false engine.runs == [["rm", "-f", "dev-prewarm-test"]] end