Skip to content

Fix SSH commands failing when the task holds over 1024 file descriptors - #74211

Merged
potiuk merged 3 commits into
apache:mainfrom
firasbouzazi:fix-ssh-select-fd-limit
Oct 5, 2026
Merged

potiuk merged 3 commits into
apache:mainfrom
firasbouzazi:fix-ssh-select-fd-limit

Conversation

@firasbouzazi

Copy link
Copy Markdown
Contributor

select.select() cannot watch a descriptor numbered 1024 or higher, so SSHHook.exec_ssh_client_command failed with "filedescriptor out of range in select()" once the task process had that many descriptors open, whatever its open-file limit. Task SDK workers reach that count far more easily than Airflow 2 task processes did.

closes: #74205


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

select.select() cannot watch a descriptor numbered 1024 or higher, so
SSHHook.exec_ssh_client_command failed with "filedescriptor out of range
in select()" once the task process had that many descriptors open,
whatever its open-file limit. Task SDK workers reach that count far more
easily than Airflow 2 task processes did.

closes: apache#74205
Comment thread providers/ssh/tests/unit/ssh/hooks/test_ssh.py Fixed
The regression test generates the server's key itself, so the client can
trust exactly that key and keep paramiko's default rejection of unknown
host keys rather than accepting whatever key it is offered.
@firasbouzazi

Copy link
Copy Markdown
Contributor Author

Hello @dabla @Dev-iL ,

The test fixture generates a 2048-bit RSA host key on each run, which adds a little time. If that's a concern, I can switch to paramiko.ECDSAKey.generate(), which is nearly instant, or a 1024-bit RSA key.

@Dev-iL

Dev-iL commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

@firasbouzazi I think the issue with RSA is not so much speed, like the fact it's considered outdated. I think ECDSA is fine since paramiko can't generate Ed25519, or better yet: MLKEM via cryptography

I don't think it matters much for a test, unless we're talking a multi-minute penalty. So just go with the fastest algo.

RSA is considered outdated, and ECDSA keys are also the fastest paramiko can generate.
@firasbouzazi

firasbouzazi commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

@firasbouzazi I think the issue with RSA is not so much speed, like the fact it's considered outdated. I think ECDSA is fine since paramiko can't generate Ed25519, or better yet: MLKEM via cryptography

I don't think it matters much for a test, unless we're talking a multi-minute penalty. So just go with the fastest algo.

Done :) @Dev-iL

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Moving from select() to selectors.DefaultSelector fixes the FD_SETSIZE limit without changing the read and shutdown logic, and the in-process server test shows it with real descriptors above 1024.

One follow-up: providers/ssh/tunnel.py (_serve_forever) has the same select() call. There the ValueError is caught and the forwarding thread just exits, so with more than 1024 descriptors open, SSHHook.get_tunnel() leaves a local port that accepts connections but never forwards them. Since channels come and go in that loop, it needs register/unregister calls rather than a drop-in swap. Would you be up for a follow-up PR?


Drafted-by: Claude Code (Opus 5.5); reviewed by @potiuk before posting

@potiuk
potiuk merged commit eb89b40 into apache:main Oct 5, 2026
84 checks passed
@boring-cyborg

boring-cyborg Bot commented Oct 5, 2026

Copy link
Copy Markdown

Awesome work, congrats on your first merged pull request! You are invited to check our Issue Tracker for additional contributions.

viukpe added a commit to viukpe/airflow that referenced this pull request Oct 5, 2026
The port-forwarding loop in SSHTunnel._serve_forever watched its sockets
and channels with select.select(), which cannot handle a file descriptor
numbered >= FD_SETSIZE (1024) and raises 'filedescriptor out of range in
select()'. That ValueError was caught and the forwarding thread exited,
leaving a local port that accepts connections but never forwards them.
Task SDK workers reach a high descriptor count easily.

Replace select.select() with selectors.DefaultSelector (epoll/poll),
which has no FD_SETSIZE ceiling. The watched set is rebuilt each loop
iteration (mirroring the previous select() behaviour) because forwarded
channels come and go between iterations.

Add regression tests: a single high-fd forwarding check and an
end-to-end concurrent/sequential forwarding test over a real SSH server
under a high fd count. Both fail on the old select() implementation.

Follow-up to apache#74211 (same select() ceiling in SSHHook).
@firasbouzazi

Copy link
Copy Markdown
Contributor Author

Approving. Moving from select() to selectors.DefaultSelector fixes the FD_SETSIZE limit without changing the read and shutdown logic, and the in-process server test shows it with real descriptors above 1024.

One follow-up: providers/ssh/tunnel.py (_serve_forever) has the same select() call. There the ValueError is caught and the forwarding thread just exits, so with more than 1024 descriptors open, SSHHook.get_tunnel() leaves a local port that accepts connections but never forwards them. Since channels come and go in that loop, it needs register/unregister calls rather than a drop-in swap. Would you be up for a follow-up PR?

Drafted-by: Claude Code (Opus 5.5); reviewed by @potiuk before posting

Sure :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants