Replies: 2 comments
Thanks for putting this together @davelopez. The hybrid split is the right call and Phase B is close to shippable. I'd like to push back on Phase A, though — I think it's both larger than it needs to be and has a correctness problem at its center. Two live bugs also turned up while I was checking the plan against the tree. Phase A's central decision doesn't survive the requirementThe plan decides on deterministic
Change a weight,
object_store_id = random.choice(self.weighted_backend_ids) # objectstore/__init__.py:1515
obj.object_store_id = object_store_id # ...and persisteddef __filesystem_monitor(self, sleeper): # objectstore/__init__.py:1495
...
self.weighted_backend_ids = new_weighted_backend_ids # mutates the list liveThe persisted id is exactly what makes runtime mutation safe. The plan copies the weighted-list pattern and drops the persistence that makes it correct. "Mirrors how
|
|
xref #23121 (comment) Note: We should add a way to detect when a JWD has been cleaned already (maybe writing a specific token to the working_directory column) to exclude it from the cleaning task. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
A draft plan from GLM 5.2 merging issues #15616 and #20062 that we can briefly discuss in the next meeting.
TL;DR
Resolve Galaxy issues #15616 and #20062 with a hybrid architecture that treats the two concerns as genuinely different (as the code confirms they are):
Job Working Directories (JWD) — decouple from the object store entirely. JWD becomes a job concern: a global weighted pool of JWD roots in
galaxy.yml, selectable per-job via ajob_working_directory_pooldestination param (TPV-settable), with the chosen root persisted on theJobrow. This matches jmchilton's design intent, mirrors howobject_store_idalready flows, and mirrors Pulsar's existing decoupling of compute JWD from the object store.Object store cache — keep in object store config (it's a storage concern), but extend the
cache:block to accept a list of weighted dirs with deterministic hash sharding. Use a stable hash over a stable shard key (not Python's process-randomizedhash()), and choose the shard key so related files (e.g. a dataset's extra_files tree) stay co-located. No search, no per-object DB column — the remote is authoritative so a cache miss just re-downloads.Backward compat — deprecate the old single-path/single-extra_dir config with a notice (mvdbeek's suggestion); only new jobs use new JWD routing. Single cache path remains the default.
Plan Details
Plan: Multi-Cache & Multi-JWD Support for Object Stores
Steps
Phase A — JWD Decoupling (job concern)
A1. Add a
job_working_directory_poolsconfig togalaxy.ymlschema (lib/galaxy/config/schemas/config_schema.ymlnear L886job_working_directory) and the typed attrs dataclass (lib/galaxy/config/_galaxy_config_schema_attributes.pyL99). Shape: a list of named pools, each a list of weighted dirs:Resolve paths via
_in_data_dir/ abspath inlib/galaxy/config/__init__.py(near L938 wherejobs_directoryis resolved). Keepjob_working_directoryas the implicit single-dirdefaultpool (backward compat). Parallel with A2.A2. Add a
JobWorkingDirectoryPoolresolver service (new filelib/galaxy/jobs/working_directory.pyor extendlib/galaxy/job_execution/setup.py). Responsibilities:select_pool(job, destination_params) -> Pool: readjob_working_directory_poolviaget_destination_configuration(the existing chain atlib/galaxy/jobs/__init__.py:1641/lib/galaxy/model/__init__.py:2430), default todefaultpool.select_dir(pool, job) -> path: deterministic weighted selection —hash(job.id) % total_weightinto the weighted list (reuse theDistributedObjectStorerepeat-id-weight-times pattern from__init__.py:1410-1436). Deterministic by job id so re-derivation after restart is stable (no DB column needed for the dir choice, but see A4 for persistence of the pool id).get_working_directory(job) -> path:<selected_dir>/<directory_hash_id(job.id)>/<job.id>(mirrorDiskObjectStore._construct_pathat__init__.py:1000).create_working_directory(job):safe_makedirsthe path. Depends on A1.A3. Add a
job_working_directory_pool_idcolumn to theJobmodel (lib/galaxy/model/__init__.py:1661nearobject_store_id) — aTrimmedString(255)nullable column. Persist the selected pool id so cleanup and re-derivation are stable. Add an Alembic migration inlib/galaxy/model/migrations/. Parallel with A2.A4. Refactor
create_working_directory_for_job(lib/galaxy/job_execution/setup.py:331) to use the new resolver (A2) instead ofobject_store.create/get_filename(..., base_dir="job_work"). Signature becomescreate_working_directory_for_job(app, job)(or accept the resolver). Update its 2 callers:JobWrapper._create_working_directory(lib/galaxy/jobs/__init__.py:1371) andlib/galaxy/tools/actions/history_imp_exp.py:189. Depends on A2, A3.A5. Refactor
JobWrapper.working_directory/working_directory_exists/clear_working_directoryproperties (lib/galaxy/jobs/__init__.py:1347-1382) to re-derive the path via the resolver (A2) using the persisted pool id (A3), instead ofobject_store.get_filename(..., base_dir="job_work"). Remove the gating onjob.object_store_id(L1023, L1853) for JWD creation — JWD no longer depends on object store selection. Depends on A4.Also preserve current
clear_working_directorybehavior: archive old contents under_cleared_contents/<timestamp>before recreating the working directory (today this is done via object storeextra_dircalls).A6. Update
cleanup_jwdscelery task (lib/galaxy/celery/tasks.py:799):Job.object_store_id.isnot(None)(L807) so JWD cleanup works whenobject_store_idis NULL.job_working_directory_pool_idset.A7. Deprecate the
extra_dirs[type=job_work]object store config path. InBaseObjectStore.__init__(lib/galaxy/objectstore/__init__.py:446), keep seedingextra_dirs["job_work"]fromconfig.jobs_directoryfor backward compat but emit a deprecation log warning when a backend config explicitly setstype: job_workinextra_dirs. Document that JWD is now configured viajob_working_directory_poolsingalaxy.yml. Keep runtime support in place for legacy plugins/config parsers that still requirejob_work(notably XML-era backends) until a later removal phase. Depends on A5; parallel with A6.A8. Document the new
job_working_directory_pooldestination param inlib/galaxy/config/sample/job_conf.sample.yml(by example, liketmp_dir/native_specification) and indoc/source/admin/. Note TPV can set it. Parallel with A7.Phase B — Multi-Cache Dirs (storage concern)
B1. Extend the
cache:config block to accept a list of weighted dirs, with backward compat for the single-dict form. New YAML shape:Single-dict form (
cache: {path, size}) remains valid and maps to a one-element list. Updateparse_caching_config_dict_from_xml(lib/galaxy/objectstore/caching.py:123) to read multiple<cache>elements (currently only reads the first). Parallel with B2.B2. Refactor
CachingConcreteObjectStore(lib/galaxy/objectstore/_caching_base.py):staging_path: str(L30) withstaging_paths: list[CacheDirConfig](each{path, weight, size}), keeping astaging_pathproperty for backward compat returning the first._get_cache_path(L105): stable hash modulo total weight into the weighted list, thenos.path.join(selected_path, rel_path).extra_filestrees.self.staging_pathjoins in_create(L188, L195) to route through_get_cache_path. Same forrucio.py:425,432andpithos.py:204,212_createoverrides. Depends on B1._ensure_staging_path_writableand_caching_allowedto operate per selected shard/target, not a single globalstaging_path/cache_target. Depends on B1.B3. Update
cache_target/cache_targets():cache_targetproperty (_caching_base.py:377) → emit oneCacheTargetper shard dir (each with its ownsize/limit). Or deprecate the singular property and overridecache_targets()(__init__.py:861) to return the list directly.NestedObjectStore.cache_targets(__init__.py:1236) already flattens lists — no change needed; address the existing TODO about de-duplication if shards share a path._start_cache_monitor_if_needed(_caching_base.py:386): spawn oneInProcessCacheMonitorper shardCacheTarget(or iterate), and update shutdown bookkeeping to stop all monitors cleanly. The celery path (clean_object_store_cachesatcelery/tasks.py:746) already iteratescheck_caches(list)— no change. Depends on B2.B4. Update each cloud backend's
__init__to parse the new list-form cache config while preserving the single-path fallback:s3_boto3.py:206,azure_blob.py:142,cloud.py:54,s3.py:187,onedata.py:113,rucio.py:343,irods.py:229. Extract a shared helper (e.g.parse_cache_config(config, config_dict)incaching.py) to eliminate the duplicated inline parsing. Fix the irods bug (irods.py:230:cache_sizefalls back toobject_store_cache_pathinstead ofobject_store_cache_size). Keep XML parsing backward compatible by mapping one-or-many<cache>elements into the same normalized internal shape. Leavepithos.py(ignores cache) as-is but document it. Depends on B1; parallel with B3.B5. Update
to_dictin all 7 backends to serialize the list-form cache config. Depends on B4.B6. (Optional) Add a
Cachepydantic model tolib/galaxy/objectstore/templates/models.pyand acachefield to each*ObjectStoreConfigurationso user-defined/template stores get typed sharded cache config. Currently cache is entirely untyped in templates. Depends on B4; can defer.Phase C — Tests & Docs
C1. Unit tests for the JWD resolver (A2): pool selection from destinationparams, deterministic weighted dir selection, path construction, backward compat with single
job_working_directory. _Depends on A5.C2. Unit tests for cache sharding (B2): deterministic shard assignment (same relpath → same shard), weight distribution, backward compat with single-path config,
cache_targets()emitting N targets. _Depends on B3.C3. Integration tests: extend
test/integration/objectstore/_base.py(L36) and related object store integration tests to cover multi-JWD and multi-cache configs. Verifycleanup_jwdscleans across all JWD roots. Depends on A6, B3.C4. Update sample configs:
lib/galaxy/config/sample/object_store_conf.sample.yml(cache list form),lib/galaxy/config/sample/galaxy.yml.sample(job_working_directory_pools),lib/galaxy/objectstore/examples/(add multi-cache examples). Updatedoc/source/admin/object store docs. Depends on A8, B5.Verification
pytest lib/galaxy/jobs/test_working_directory.py— assert deterministic dir selection per job id, weight distribution over 10k jobs is within tolerance,job_working_directory_pooldestination param overrides pool, missing pool falls back todefault, singlejob_working_directoryconfig still works.pytest lib/galaxy/objectstore/test_caching.py— assert samerel_pathalways maps to same shard,cache_targets()returns one target per shard, single-path config yields one target (backward compat),_get_cache_pathdistributes across shards per weight.bash run_tests.sh -lib galaxy.objectstoreand the integration suite intest/integration/objectstore/— verify multi-cache config loads, files round-trip across shards, eviction cleans each shard.cleanup_jwdsagainst jobs withjob_working_directory_pool_idset andobject_store_idNULL — assert dirs are cleaned (regression for the dropped filter).cleanup_jwdsagainst old jobs withoutjob_working_directory_pool_idand verify fallback cleanup still removes legacy JWD paths.job_working_directoryand singlecache.pathconfig — assert no errors, deprecation warnings logged, jobs run with JWD under the legacy path.extra_filesdirectory tree is consistently addressable and not split across shards in a way that breaksdir_onlyretrieval semantics.job_working_directory_pool: pulsarfor pulsar-routed tools — assert those jobs' JWDs land in the pulsar pool dir (validates cat-bro's use case).job_working_directory_pool_idcolumn added, nullable, indexed.mypy lib/galaxy/objectstore/ lib/galaxy/jobs/andruff checkpass on changed files.Decisions
base_dir; cache is a storage staging concern), Pulsar already decouples compute JWD, and jmchilton (assigned maintainer) explicitly called object-store-based JWD a "design limitation/bug."job.id(not random): ensures the same job always re-derives the same JWD after a restart, so no DB column is needed for the dir choice. The pool id is persisted (A3) because pool selection is policy-driven (TPV/destination) and must be recoverable.extra_dirs[job_work]and singlecache.pathkeep working with a deprecation notice. Only new jobs use the new JWD routing. No data migration of existing jobs required.CacheTarget/limit — simpler and matches existingcheck_cachesiteration), the pydanticCachetemplate model (B6 is optional/deferrable), changing Pulsar's remote JWD handling (already decoupled).Further Considerations
Should the JWD pool selection be random instead of deterministic? Deterministic (by
job.id) is recommended so re-derivation after restart is stable without persisting the dir choice. Random would give better load distribution but requires persisting the chosen dir on the job (another column). Option A: deterministic hash (recommended, no extra column). Option B: random + persist dir on job. Option C: weighted random with a filesystem-monitor thread that prunes full dirs (mirrorsDistributedObjectStore.__filesystem_monitor) — most flexible, most complex.Should
max_percent_fullbe supported per JWD dir / cache shard? bgruening's original issue requestedmaxpctfullfor JWDs. TheDistributedObjectStorealready has a filesystem monitor that prunes backends overmax_percent_full. This could be reused for both JWD dirs and cache shards but adds a monitoring thread per dir. Recommendation: defer to a follow-up; the deterministic sharding already spreads load. Option A: defer (recommended). Option B: addmax_percent_fullper dir with a shared filesystem monitor.Should the cache support a shared limit across shards? Currently each
CacheTargetis evicted independently to its own limit. With shards of unequal size this is fine; with a single logical cache split across mounts, a shared limit would be more accurate. Recommendation: keep per-shard limits (simpler, matches existingcheck_caches). Option A: per-shard limits (recommended). Option B: shared limit requiring a new aggregation path incaching.py.All reactions