Skip to content

Make JWD a job concern#23133

Open
davelopez wants to merge 28 commits into
galaxyproject:devfrom
davelopez:jwd_as_job_concern
Open

Make JWD a job concern#23133
davelopez wants to merge 28 commits into
galaxyproject:devfrom
davelopez:jwd_as_job_concern

Conversation

@davelopez

@davelopez davelopez commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Implements Phase A of the plan discussed in #23107: decouple the job working directory (JWD) from the object store and make it a job-level concern, resolved once from destination params and persisted on the Job row. This closes the proposed use case from #15616 (overriding job_working_directory per destination via TPV) and lays the groundwork for the weighted-pool / drain behavior requested in #20062.

The approach follows the counter-proposal in the discussion: mirror how object_store_id already flows (read from destination params in enqueue(), persisted on the row) rather than hash-deriving from a weighted pool. No config schema, no resolver service, no weighting code in Galaxy.

Related TPV pull request galaxyproject/total-perspective-vortex#202

What changed

  • New Job.working_directory column (String(1024), mirroring the existing Task.working_directory) added via migration e96dd6fd5863. Persisted when a destination specifies job_working_directory; NULL means "use the legacy object-store path".

  • JobWorkingDirectory class in lib/galaxy/job_execution/setup.py centralizes all JWD operations (resolve, exists, create, delete, cleared_contents_base) behind a single handle, replacing the open-coded object_store.get_filename(job, base_dir="job_work", dir_only=True, obj_dir=True) calls scattered across the codebase. It transparently picks the custom-path strategy when job.working_directory is set and falls back to the object-store strategy otherwise, so callers never branch on the column.

    The custom path is treated as a parent/base: Galaxy appends <directory_hash_id(job.id)>/<job.id>/ for per-job isolation, mirroring the object-store layout. This means a job_working_directory destination param (e.g. set by TPV from a weighted pool, see Add weighted_choice helper for weighted random selection in TPV configs total-perspective-vortex#202) is the base directory, and each job gets its own subdirectory under it.

  • _set_working_directory in MinimalJobWrapper resolves job_working_directory from the persisted job.destination_params (not via get_destination_configuration, which would fall back to the config default and persist it even for destinations that don't specify one) and writes it to job.working_directory. Called from _setup_working_directory before anything is created on disk. Reset to None on resubmit when the new destination no longer specifies a custom path, so stale values don't survive.

  • validate_working_directory_path guards every disk operation on a custom path (and at set-time): refuses empty, relative, or filesystem-root paths, since job.working_directory is an admin/TPV-supplied string that shutil.rmtree obeys literally.

  • Resubmit reorder in resubmit.py: set_job_destination (which persists the new destination_params) now runs before clear_working_directory, so the latter's _setup_working_directory_set_working_directory reads the new params and resolves the new JWD correctly. Previously the order was inverted, so a resubmitted job resolved its JWD against the stale destination. clear_working_directory now flushes so the mutated working_directory column survives the sa_session.refresh() that mark_as_resubmitted() performs.

  • Init-order fix in MinimalJobWrapper.__init__: self.params is now initialized before _setup_working_directory, since that call chain reaches get_destination_configuration via the job_destination property.

  • Callers migrated to JobWorkingDirectory: MinimalJobWrapper (working_directory, working_directory_exists, _create_working_directory, clear_working_directory, finish cleanup), JobManager stdout/stderr tail, SetMetadataTool.exec_after_process, ExportHistoryToolAction, the Celery cleanup_jwds task, tools/actions/metadata.py (out-of-band metadata job), and webapps/galaxy/api/job_files.py (Pulsar staging security check). The previously out-of-band sites that used extra_dir=str(job.id) are now also routed through JobWorkingDirectory, which resolves to the same path via obj_dir=True.

Why

  • The JWD is a job concern, not an object store concern. The object store manages datasets and their lifecycle; the working directory is transient scratch space tied to a specific job execution. Coupling them forces every JWD lookup to go through object-store resolution, which assumes a backing store has already been assigned — an assumption that doesn't hold during enqueue, resubmit, or cleanup.
  • Distribution across multiple JWD mounts. Persisting a per-job path lets admins (via TPV or destination params, see Add weighted_choice helper for weighted random selection in TPV configs total-perspective-vortex#202) spread jobs across several job_working_directory mounts, enabling load balancing across disks or nodes. Because each job's JWD is resolved once and stored on the row, in-flight jobs are unaffected when the distribution rule changes.
  • Persistence stops being optional. Galaxy cannot re-derive what TPV decided, so the value is persisted — the "mutate weights and lose in-flight JWDs" bug from the original Phase A draft becomes unrepresentable.
  • Drain works. Change the rule; in-flight jobs keep their persisted path.
  • Recovery is free. TPV params land in job.destination_params, and recovery rebuilds from from_job=job without re-running the rule.
  • Reads stop depending on the destination. working_directory no longer raises ObjectNotFound for jobs whose object store hasn't been assigned yet.

Tests

  • New unit tests in test/unit/job_execution/test_job_working_directory.py covering JobWorkingDirectory per-job isolation of custom paths (resolve, create, delete, cleared_contents_base, validation).
  • Updated test/unit/app/test_tasks.py (cleanup task), test/integration/test_job_files.py, and test/integration/test_pulsar_embedded.py to use the JobWorkingDirectory abstraction.

How to test the changes?

License

  • I agree to license these and all my past contributions to the core galaxy codebase under the MIT license.

# set_job_destination() assigns job.destination_params = new_destination.params
# and flushes, so when clear_working_directory() → _setup_working_directory()
# → _set_working_directory() reads self.job_destination.params, which now
# holds the *new* params, not the stale values from the prior attempt.

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.

A comment here is good - maybe just the first line and last line though - these four lines in the middle seem like noise to me.

Really nice catch on the location of delay bug - I think this is a very long existing bug with existing issues?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I've reduced that comment. Of course it wasn't me who detected the bug 😅

# _setup_working_directory() → _set_working_directory() mutates the
# working_directory column, so the column survives the sa_session.refresh()
# that mark_as_resubmitted() performs at the end of this flow even when no
# handler commit runs below.

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.

Just drop this second comment? You're literally restating abstraction details the abstraction is meant to hide I think? Feel free to correct me if I am wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are wrong in very rare cases, and this is not one of them 😆

For some reason, my agent was obsessed with writing archaeology comments all over this code, and I was not familiar enough with it to decide what was important to keep and what wasn't. So thank you again.

Comment thread lib/galaxy/jobs/__init__.py
Comment thread lib/galaxy/tools/actions/metadata.py Outdated
# is resolved, so job.working_directory is None. This site uses
# extra_dir=str(job.id), a different path scheme than the obj_dir=True sites
# that JobWorkingDirectory manages. Keep the direct object-store call so the
# path matches what later metadata file lookups expect.

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.

These are accidentally different instead of purposefully different though right? Can you have your agent research if these could be migrated to use the new abstraction and if not if we could do a parallel abstraction in the same job_execution/ file for this - so we're keeping like-callers and like concerns close together? This can be a follow up as long as there is an issue I think.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There was a bit of back and forth in the commits about this (and the next comment). I think it was the test failures that triggered the change, and again, I was not comfortable enough arguing with my agent, who was very sure this was the correct approach 😅 Turns out I asked again, and now it thinks it is perfectly fine to use the same abstraction and forget about the extra_dir thing... here is what it says now:

The earlier test failures that drove the revert were likely caused by the extra_dir/legacy_extra_dir parameter being passed in addition to obj_dir=True inside JobWorkingDirectory (see the a34f6d2 version of setup.py — it passed both obj_dir=True AND extra_dir=extra_dir to the object store). That combination produces /000/001/<job.id>/<job.id> — a doubled path — which is genuinely different and would break lookups. My migration drops extra_dir entirely and relies on obj_dir=True alone, which matches the original path exactly.

🤷‍♂️

Comment thread lib/galaxy/webapps/galaxy/api/job_files.py Outdated
@davelopez
davelopez marked this pull request as draft July 21, 2026 17:23
@davelopez

Copy link
Copy Markdown
Contributor Author

While playing with the TPV pull request I'm working on, I noticed that the job_working_directory assigned based on weights from TPV should be the base JWD, and Galaxy still needs to append the /<hash/<job_id> to it; otherwise, all jobs assigned to that JWD use the exact same directory 😅

I'll fix that...

@davelopez

davelopez commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Now it works in combination with galaxyproject/total-perspective-vortex#202

With a job_conf.yml similar to this one (make sure to replace <your galaxy root> and set your job_config_file: job_conf.yml in galaxy.yml):

runners:
    local:
        load: galaxy.jobs.runners.local:LocalJobRunner
        workers: 4

execution:
    default: tpv_dispatcher
    environments:
        tpv_dispatcher:
            runner: dynamic_tpv
            tpv_config_files:
                - /<your galaxy root>/config/tpv_mapping.yml

And this config/tpv_mapping.yml rule (make sure to replace <some path>):

global:
    default_inherits: default
    context:
        jwd_pool:
            - value: /<some path>/jobs_directory/fast
              weight: 3
            - value: /<some path>/jobs_directory/slow
              weight: 1

tools:
    default:
        cores: 1
        mem: 2
        params:
            job_working_directory: "{helpers.weighted_choice(jwd_pool)}"
        scheduling:
            require: []
            prefer:
                - general
            accept: []
            reject: []

destinations:
    local:
        runner: local
        max_accepted_cores: 4
        max_accepted_mem: 8
        scheduling:
            prefer:
                - general

The JWD of each job will look something like /<some path>/jobs_directory/fast/<hash>/<job_id>; in the database, the job.working_directory will be the base directory that got selected in TPV according to the weight; in this case, /<some path>/jobs_directory/fast.

davelopez added 19 commits July 23, 2026 16:38
- Eliminates construction-order sensitivity by reading the directory column fresh on each access
- Clarifies parameter naming to restrict legacy object-store subdirectory handling
- Establishes a canonical path validator enforced at both assignment and usage times
- Replaces inline path checks with the centralized validation function
- Defers session persistence to callers to improve transaction management flexibility
- Ensures working directory mutations survive database refresh operations during job resubmission
- Adjusts documentation to reflect deferred session persistence behavior
- Migrates existing callers to use the renamed legacy parameter
- Refactors integration tests to consistently utilize the working directory abstraction
Adds an existence check before attempting to remove the custom working directory path. Prevents exceptions if the directory was already cleaned up or never created.
Refactors metadata actions and job files API to bypass the standard working directory class. Direct object store calls maintain compatibility with a specific legacy path scheme required by the site. Removes unused imports.
Working directory configuration now triggers during initialization and references the params attribute. Initializing it beforehand prevents an AttributeError when resolving the job destination.
The previous implementation relied on a configuration fallback that incorrectly mapped the global object-store base directory to individual jobs. Reading directly from the destination parameters prevents this unintended override and ensures resubmitted jobs correctly resolve their custom working directories.
The extra directory argument was only relevant for historical object store paths and introduced unnecessary complexity. Dropping it streamlines the API and eliminates outdated error checks.
Reads destination parameters directly from the persisted job object to prevent incorrectly storing the default configuration value. Bypasses the mapper's cached destination to resolve stale data issues during resubmit and reorder operations. Removes outdated comments regarding legacy fallback behavior.
Removes excessive implementation details regarding session flushing and commit paths that cluttered the workflow.
Replaces direct object-store calls with a centralized approach for creating and resolving job working directories.
Additionally, directory creation now fails loudly if a path already exists, preventing silent overwrites from ID collisions or stale runs.
Streamlines documentation by removing verbose implementation details and historical design rationales.
Prevents path collisions and ensures consistent directory sharding when custom working directories are configured. Validates resolution, creation, cleanup, and cleared contents base logic while confirming the default object-store behavior remains intact.
Replaces direct object store and filesystem mocking with a dedicated test fixture. This simplifies setup, removes unnecessary temporary directory manipulations, and shifts focus to the core logic and error handling.
This improves code readability and ensures paths are explicitly validated before use.
@davelopez
davelopez force-pushed the jwd_as_job_concern branch from cd8c2e9 to bb4740d Compare July 23, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

2 participants