Cache private keys, reuse remote clients, and batch DAG status queries - #38
Open
romer8 wants to merge 4 commits into
Open
Cache private keys, reuse remote clients, and batch DAG status queries#38romer8 wants to merge 4 commits into
romer8 wants to merge 4 commits into
Conversation
Job status polling re-decrypted the private key and rebuilt the SSH client on every property access, and queried each DAG node individually.
…ameters are unchanged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Tethys polls job status through condorpy on every UI refresh. Each poll was far more
expensive than it needed to be. Instrumenting a single status poll of a 4-node DAG
against an HTCondor 8.8.2 pool:
condor_q/condor_history(14 calls)Under concurrent load this starves the calling application: 8 concurrent viewers of 10
running workflows drove poll latency to 20 s and cut throughput 9x, with the scheduler
itself idle at 0.2% CPU.
Why it was slow
The private key was re-decrypted on every property access.
set_scheduler()built a newRemoteClienteach time it was called, andRemoteClient.__init__callsparamiko.RSAKey.from_private_key_file(), which runs a KDF (~0.47 s) for apassphrase-protected key. Callers invoke
set_scheduler()from a property, so this happenedseveral times per poll.
RemoteClientalready caches its SSH transport, but replacing theclient object discarded that cache, forcing a fresh handshake too.
Node statuses were queried one at a time.
_update_statuses()looped overnode_setcalling
job.status, and each of those runscondor_q ... && condor_history .... ADAGManJobIDconstraint already returns every node of the DAG in one query, so the per-noderound trips were redundant. On a 14-node DAG this is ~40 remote commands per poll.
Reading
node_setperformed a remote query. The property calledupdate_node_ids()onevery access, so merely iterating the nodes cost a round trip.
The changes
load_private_key()caches decrypted keys on(path, passphrase, mtime). The mtimemeans a rotated key file is picked up rather than served stale. Guarded by a lock, since
callers may be threaded.
set_scheduler()reuses the existingRemoteClientwhen the connection parameters areunchanged, via
RemoteClient.matches(), so the cached transport survives._remote_idis preserved on the reuse path -- it names the job's remote working directory, so minting
a new one would orphan the remote workspace.
node_statuses_by_cluster_id()fetches every node's status in one query;_update_statuses()uses it and falls back to per-node queries if it fails.node_setresolves node ids once per object instead of on every read.add_node()clears the flag so a newly added node is still resolved.
Results
Same live DAG, before and after: 2.03 s -> 0.51 s per poll (4x), with identical
correctness -- same job status, same node count, same per-node statuses, and the same 14
remote commands issued, confirming no work was skipped. Key decryptions per poll went 5 -> 0
(warm) and clients 5 -> 1.
The stronger evidence for change 3 is that
Workflow.statusesbecomes independent of DAGsize. Measured against 26-node DAGs with 24 nodes expanded, on a caller pinned to 0.5 CPU,
with L concurrent viewers each monitoring their own live workflow:
Workflow.statusespathjob.statuspathThe batched path stays flat at ~0.5 s and degrades gracefully under concurrency instead of
collapsing. Zero failures at every level.
What this does not fix
The right-hand column above is the honest limit of this PR. A caller that asks each node for
its status individually --
for node in workflow.node_set: node.job.status-- still pays onecondor_q/condor_historypair per node, measured at ~115 ms per expanded node. condorpycannot batch that from the inside, because the request arrives one node at a time.
node_statuses_by_cluster_id()is the primitive such callers should move to; changes 1, 2and 4 reduce the cost of each individual call but not their number. Tethys has a companion
change (tethysplatform/tethys#1294) that consumes the batched method and persists the result,
which is what removes the per-node pattern on its side.
Reproducing the measurements
One caveat for anyone verifying this: a DAG node that DAGMan has not yet submitted has no
cluster_id, and condorpy reportsUnexpandedfor it without any remote call. If the rootnode of a test DAG runs for the duration of the test, the fan-out is never submitted and poll
cost looks flat regardless of DAG size. Give the root node a short runtime and the leaves a
long one, and confirm expansion with
condor_q -constraint "DAGManJobID==<cluster>"before trusting a number.Tests
tests/test_remote_reuse.pyadds 9 tests covering key caching (including reload on filechange), client reuse and replacement when the host changes,
_remote_idstability acrossreuse, and node id resolution behavior.
Test count goes from 72 to 81 with
python -m unittest discover. The two pre-existingerrors (
test_remote,test_integraiton.test_submit) are unchanged -- they require a localcondor pool and
~/.ssh/id_rsa, which CI provides.Verified against HTCondor 8.8.2 and 25.x.