fix(gateway): persist RLIMIT_NOFILE floor into the generated launchd plist
launchd starts children with soft nofile=256; hermes gateway start rewrites the plist and previously stripped any manually-added SoftResourceLimits, silently reintroducing EMFILE crashes under load. The plist generator now embeds the configured runtime.nofile_soft_limit so the persisted service definition and the in-process floor share one knob.
This commit is contained in:
+23
-1
@@ -4129,6 +4129,28 @@ def generate_launchd_plist() -> str:
|
||||
)
|
||||
prog_args_xml = "\n ".join(prog_args)
|
||||
|
||||
# Persist the configured RLIMIT_NOFILE floor into the service definition
|
||||
# itself. launchd starts children with a soft limit of 256 by default;
|
||||
# without this block every plist rewrite (e.g. `hermes gateway start`)
|
||||
# would silently strip a manually-added limit and reintroduce EMFILE
|
||||
# crashes under load. The in-process floor (resource_limits.py) still
|
||||
# applies as a second layer for non-launchd launches.
|
||||
nofile_block = ""
|
||||
try:
|
||||
from hermes_cli.resource_limits import configured_nofile_soft_limit
|
||||
|
||||
nofile_target = configured_nofile_soft_limit()
|
||||
except Exception:
|
||||
nofile_target = None
|
||||
if nofile_target:
|
||||
nofile_block = f"""
|
||||
<key>SoftResourceLimits</key>
|
||||
<dict>
|
||||
<key>NumberOfFiles</key>
|
||||
<integer>{nofile_target}</integer>
|
||||
</dict>
|
||||
"""
|
||||
|
||||
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
@@ -4175,7 +4197,7 @@ def generate_launchd_plist() -> str:
|
||||
|
||||
<key>ExitTimeOut</key>
|
||||
<integer>25</integer>
|
||||
|
||||
{nofile_block}
|
||||
<key>StandardOutPath</key>
|
||||
<string>{log_dir}/gateway.log</string>
|
||||
|
||||
|
||||
@@ -65,6 +65,18 @@ def _configured_nofile_soft_limit(
|
||||
return raw_value
|
||||
|
||||
|
||||
def configured_nofile_soft_limit(
|
||||
config: Mapping[str, Any] | None = None,
|
||||
) -> int | None:
|
||||
"""Public accessor for the resolved ``runtime.nofile_soft_limit`` target.
|
||||
|
||||
Used by service-definition generators (e.g. the launchd plist) so the
|
||||
persisted service limits and the in-process floor share one config knob.
|
||||
Returns ``None`` when the adjustment is disabled or unresolvable.
|
||||
"""
|
||||
return _configured_nofile_soft_limit(config)
|
||||
|
||||
|
||||
def apply_nofile_soft_limit(
|
||||
config: Mapping[str, Any] | None = None,
|
||||
) -> bool:
|
||||
@@ -116,4 +128,5 @@ def apply_nofile_soft_limit(
|
||||
__all__ = [
|
||||
"DEFAULT_NOFILE_SOFT_LIMIT",
|
||||
"apply_nofile_soft_limit",
|
||||
"configured_nofile_soft_limit",
|
||||
]
|
||||
|
||||
@@ -232,6 +232,35 @@ class TestGeneratedSystemdUnits:
|
||||
assert str(local_bin) in plist
|
||||
assert str(profile_node_bin) not in plist
|
||||
|
||||
def test_launchd_plist_persists_configured_nofile_soft_limit(self, monkeypatch):
|
||||
"""The generated plist must carry SoftResourceLimits/NumberOfFiles so a
|
||||
plist rewrite by `hermes gateway start` cannot strip the FD floor and
|
||||
reintroduce EMFILE crashes (launchd default soft limit is 256)."""
|
||||
import hermes_cli.resource_limits as resource_limits
|
||||
|
||||
monkeypatch.setattr(
|
||||
resource_limits, "configured_nofile_soft_limit", lambda config=None: 65536
|
||||
)
|
||||
|
||||
plist = gateway_cli.generate_launchd_plist()
|
||||
|
||||
assert "<key>SoftResourceLimits</key>" in plist
|
||||
assert "<key>NumberOfFiles</key>" in plist
|
||||
assert "<integer>65536</integer>" in plist
|
||||
|
||||
def test_launchd_plist_omits_nofile_block_when_disabled(self, monkeypatch):
|
||||
"""runtime.nofile_soft_limit: 0/false/null disables the adjustment; the
|
||||
plist must then not contain a SoftResourceLimits block at all."""
|
||||
import hermes_cli.resource_limits as resource_limits
|
||||
|
||||
monkeypatch.setattr(
|
||||
resource_limits, "configured_nofile_soft_limit", lambda config=None: None
|
||||
)
|
||||
|
||||
plist = gateway_cli.generate_launchd_plist()
|
||||
|
||||
assert "SoftResourceLimits" not in plist
|
||||
|
||||
|
||||
|
||||
class TestGatewayStopCleanup:
|
||||
|
||||
Reference in New Issue
Block a user