Conversation
Extracted from Galaxy's htcondor job runner so both sides submit, remove, and read job event logs the same way. Knows nothing about either application's job model - callers map EventLogSummary and the HOLD_REASON_*/FAILURE_* keys onto their own vocabulary. Mirrored byte-for-byte into Galaxy's lib/galaxy/jobs/runners/util/condor/, following the existing util/ mirror convention, so it has to stay Python 3.7 parseable - hence the __future__ annotations import and no walrus. htcondor2 reads its configuration once per process, so a per-destination CONDOR_CONFIG needs the helper subprocess; the helper module is resolved relative to __package__ so the same file works under either package root.
Talks to HTCondor through the version 2 Python bindings rather than shelling out to condor_submit/condor_rm, and reads the job event log through htcondor2.JobEventLog rather than scraping its text. Maps the shared module's vocabulary onto Pulsar's job statuses - out-of-memory and walltime holds, held count exhaustion, and event-log failures all become FAILED. Tests run against a fake htcondor2 module using the real JobEventType integer values, so they exercise the event-log state machine without a schedd.
StatefulManagerProxy treats LOST as possibly transient and never deactivates a job reporting it - a manager also returns LOST for a job whose external id has not been recovered yet. So the two escalations here, a missing event log and a spent status-error budget, left the job active forever with its event log handle open and told the client nothing. Both now report FAILED, which is a considered judgement: they only fire once the retry budget is spent. Galaxy routes failed and lost to the same fail_job, so nothing is lost from the report and the job actually finishes. get_status still returns LOST when there is no external id at all - that one really is the transient case LOST is for. The fake htcondor2 module grows an error hook so events() can raise on demand.
MAX_STATUS_ERROR_COUNT/MAX_MISSING_LOG_COUNT counted polls, but the poll interval is configurable in both Galaxy (job_runner_monitor_sleep, default 1.0s, 0.2s in the test driver) and Pulsar, and a Pulsar job is polled by both the Pulsar monitor and Galaxy - so the grace window had no fixed meaning and could shrink to under a second. Replace with wall-clock grace periods (STATUS_ERROR_GRACE_SECONDS=30, MISSING_LOG_GRACE_SECONDS=60) tracked by first-seen timestamps on HTCondorEventLogTracker, with an injectable clock so tests drive escalation without sleeping. Also preserve RUNNING across a transient missing log, matching the sibling status-error branch. Shared module mirrored to Galaxy.
- Report a JOB_TERMINATED with TermSignal=9 as FAILED rather than COMPLETE, matching the Galaxy runner. get_status short-circuits cancelled jobs, so no equivalent of Galaxy's STOPPED/DELETED guard is needed. An OOM-killed wrapper may never write an exit code file, so COMPLETE could read as success. - Close the touched event log file instead of leaking the handle to GC. - Give ManagerInterface a concrete no-op shutdown so managers with nothing to release inherit it, and drop the three separate workarounds for its absence (getattr probe in queued_htcondor, try/except AttributeError in ManagerProxy, try/except Exception in base_drmaa - the last now uses finally, so the DRMAA session still closes but real errors are no longer swallowed).
launch() updated submit_params in place, but submit_params defaults to a shared dict, so a manager's submission_params stayed in it for the next job launched without explicit params - two managers of different configurations would submit each other's settings. Copy instead, as queued_htcondor does. Also close the touched event log rather than leaking the handle to GC, and point the module docstring at queued_htcondor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Present queued_condor and queued_htcondor under one heading with a recommendation rather than as unrelated sections. The distinction that matters to an admin is that queued_condor reports only queued/running/complete: it never inspects hold events, and reports an aborted job or a missing event log as complete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 tasks
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.
This PR description was drafted and posted by Claude (AI assistant) on jmchilton's behalf.
Summary
Adds a
queued_htcondormanager built on HTCondor's version 2 Python bindings, and puts the HTCondor mechanics inpulsar.managers.util.condor.htcondor— a module shared byte-for-byte with Galaxy'shtcondorjob runner.Galaxy-side counterpart branch: https://github.com/jmchilton/galaxy/tree/htcondor_pulsar (deliberately not a PR yet — it needs to land alongside this).
util/condor/htcondor.py+htcondor_helper.py): thehtcondor2import, schedd clients, event-log parsing, hold and failure classification, walltime/memory parsing. It knows nothing about either application's job model — callers map its vocabulary (EventLogSummary,HOLD_REASON_*,FAILURE_*) onto their own states.queued_htcondormanager: maps that vocabulary onto Pulsar's status vocabulary. Never shells out tocondor_submit/condor_rm, and reads the event log viahtcondor2.JobEventLograther than scraping its text (contrastqueued_condor).request_walltimeis translated into aperiodic_holdexpression; walltime and out-of-memory holds are classified and reported asfailedwith a message telling the user which limit to raise.CONDOR_CONFIG(htcondor2 reads its config once per process, so those cannot share a process).htcondorextra insetup.py; docs for the manager and the shared package.Stacking
Based on #485 (
terminal-job-statuses), so the diff shown here is just the HTCondor work. #485 should go in first.The dependency is real, not just convenience: the escalations below report
FAILED, which only became terminal inStatefulManagerProxyin #485.Why the code is shared rather than reimplemented
Galaxy's runner and this manager need identical answers to the same questions — is
HoldReasonCode=34an OOM hold, doesTermSignal=9mean the job was killed, has the event log gone missing. Divergence there means the same cluster event is reported two different ways depending on whether the job went through Pulsar. The module is mirrored (identical bytes in both repos) rather than depended on, because Pulsar cannot take a hard dependency on Galaxy.Two constraints follow, both noted in the module docstring:
from __future__ import annotationsfor theX | Noneannotations, and no walrus operator.diffbetween the two paths should be empty.Behavior notes
Worth a reviewer's attention:
FAILED, notLOST.StatefulManagerProxytreatsLOSTas possibly transient — a manager also returns it for a job whose external id has not been recovered yet — so a job reportedLOSTis never deactivated. These escalations only fire once the grace period has elapsed, at which point the job really is over.JOB_TERMINATEDwithTermSignal=9isFAILED, notCOMPLETE. An OOM-killed wrapper may never write its exit-code file, soCOMPLETEwould present a killed job as a successful one.get_statusshort-circuits cancelled jobs, so a SIGKILL reaching this branch was not requested by us.ManagerInterface.shutdowngains a concrete no-op, replacing three separate workarounds for managers that do not define it (agetattrprobe, atry/except AttributeErrorinManagerProxy, and atry/except Exceptioninbase_drmaa). That last one is a small behavior change: it is nowtry/finally, so the DRMAA session still always closes, but a genuine error from the parentshutdownpropagates instead of being swallowed.Validation
tox -e test-unit— 311 passed, 83 skippedtest/manager_htcondor_test.py, driven by a fakehtcondor2module whoseJobEventTypeintegers match the real library. The fake's event log is consume-once like the real one, so the tests exercise that constraint rather than papering over it. An injectable clock drives the escalation grace periods without sleeping.Relationship to
queued_condorPulsar already has a Condor manager, and the two are less independent than they look: they share
util/condor/__init__.py—submission_params(),build_submit_description(), thesubmit_prefix convention, even thejob_condor.logfilename. They diverge after submission, becausesummarize_condor_log()is a text scrape tied to the CLI's log format with no bindings equivalent.The difference that matters when choosing between them is the status vocabulary.
queued_condorreports onlyqueued/running/complete, and several failures reach the client as success:complete(e.g. a log that was never written because the filesystem was full at submit time)JOB_ABORTED(009) →complete, indistinguishable fromJOB_TERMINATED(005)JOB_HELD(012) is never scanned, so a job held for exceeding memory or wall time sits atqueuedindefinitelySHADOW_EXCEPTION(007) →runningdocs/job_managers.rstnow presents both under a single HTCondor heading with that comparison and a recommendation — preferqueued_htcondorunlesshtcondor2cannot be installed — instead of leaving them as two unrelated sections.One robustness difference is worth knowing, since it explains a design choice here: the text scrape re-derives full state from the file on every poll, so it needs no bookkeeping and survives a restart for free.
JobEventLog.events()is consume-once per handle, which is why this manager carriesrunning/held_countstate and why the terminal latch from #485 matters to it.Drive-by fixes to
queued_condorTwo bugs the new manager avoids by construction, fixed in the old one rather than left behind:
launch()didsubmit_params.update(self.submission_params)against asubmit_params={}default argument, mutating the shared default. Two managers with different configurations would submit each other's settings on any launch that omitted explicit params. Regression test added — there was no unit coverage for this manager at all, only an integration test gated oncondor_submitbeing present.open(log_path, 'w')left the handle to the garbage collector.