Skip to content

Log which ops require each resource during initialization#34005

Open
Mariomsoliman wants to merge 1 commit into
dagster-io:masterfrom
Mariomsoliman:2307-resource-origin-logging
Open

Log which ops require each resource during initialization#34005
Mariomsoliman wants to merge 1 commit into
dagster-io:masterfrom
Mariomsoliman:2307-resource-origin-logging

Conversation

@Mariomsoliman

Copy link
Copy Markdown

This closes #2307.
Right now when Dagster starts initializing resources for a run, the log just prints something like: Starting initialization of resources [io_manager, resource_a, resource_b]. That's fine if we have one or two resources, but on a job with many, it's hard to tell which op actually needed which resource. If resource_a fails to initialize, we have no idea what part of your pipeline that's going to break. This PR fixes that by adding the missing context: Starting initialization of resources: io_manager - required by the I/O layer
resource_a - required by op instances [op_one]
resource_b - required by op instances [op_two]

I added a helper, get_resource_origins_mapping, that walks a job's ops (and their hooks) and builds a mapping from each resource key to the op names that actually require it. It also handles the case where one resource depends on another , so if spark is only used because pyspark needs it, spark still gets attributed to whatever op needed pyspark. That's actually the exact example from the original issue.
I threaded job_def through the resource init call chain so this mapping can actually get built where it's needed, and it defaults to None everywhere so nothing breaks for code paths that don't have a job (like the standalone build_resources() helper).
One thing worth calling out: resources get initialized per step process under the default executor, not once for the whole run. So the log message only looks at the resources that specific process actually needs, not the whole job, otherwise you'd get one step's log showing another step's ops, which would be confusing in the opposite direction.
I also had to update dagstermill's resource setup, since it mirrors this same function and would've broken for notebook-based jobs otherwise.

I also added tests that directly cover the new mapping logic, both the new message format and the old fallback message (for when no job_def is available), and a regression test that checks that the job_def actually makes it all the way through the call chain instead of being silently dropped somewhere. Also ran this against a real job with dagster job execute to confirm the output looks right, and ran the full test suites for both dagster core and dagstermill. Any failures I found were pre-existing on main, not caused by my change.

The RESOURCE_INIT_STARTED log line previously listed only resource keys:

    Starting initialization of resources [io_manager, resource_a, resource_b].

On jobs with many resources this made it impossible to tell which op pulled
in a given resource. It now attributes each resource to the ops that require
it:

    Starting initialization of resources:
    io_manager - required by the I/O layer
    resource_a - required by op instances [op_one]
    resource_b - required by op instances [op_two]

get_resource_origins_mapping builds resource_key -> {op instance names} from
op and hook required_resource_keys, keyed by full node handle so nested and
aliased ops are unambiguous, and propagates through resource-to-resource
dependencies. job_def is threaded to the resource init generator to supply
it; when absent (e.g. build_resources) the original message is preserved.
The dagstermill resource manager is updated to match the new signature.

Resolves dagster-io#2307.
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR addresses issue #2307 by enriching the RESOURCE_INIT_STARTED log message to attribute each resource to the op instances that require it (or label it as an I/O-layer resource), making it much easier to diagnose initialization failures in jobs with many resources.

  • Adds get_resource_origins_mapping(job_def) in resources_init.py that walks all op (and hook) nodes, collects their required_resource_keys, and propagates attribution through transitive resource dependencies so that a resource pulled in only by another resource is still credited to the op that triggered the chain.
  • Threads an optional job_def parameter through resource_initialization_managerresource_initialization_event_generator_core_resource_initialization_event_generator, with None as the default so standalone build_resources() paths are unaffected; context_creation_job.py and dagstermill/manager.py are updated to pass job_def through.
  • New tests cover the mapping logic, both message formats (enriched and legacy fallback), and a monkeypatch regression that verifies job_def reaches the innermost generator rather than being silently dropped.

Confidence Score: 4/5

Safe to merge — changes are confined to a log message and its supporting plumbing; no execution logic is altered and all existing code paths have None defaults.

The falsy-empty-dict check in _resource_init_start_message means the enriched format is silently skipped for jobs where all resources are exclusively I/O-layer resources, even when job_def is available. This is a logging gap rather than a correctness issue, but it is an inconsistency in the feature's own contract that the tests don't catch.

python_modules/dagster/dagster/_core/events/init.py — the _resource_init_start_message guard on line 417

Important Files Changed

Filename Overview
python_modules/dagster/dagster/_core/events/init.py Adds _resource_init_start_message helper and threads it into resource_init_start; falsy-empty-dict check on the mapping may silently suppress the enriched format
python_modules/dagster/dagster/_core/execution/resources_init.py Adds get_resource_origins_mapping and threads job_def through the init call chain; logic is correct for typical jobs
python_modules/dagster/dagster/_core/execution/context_creation_job.py Passes job_def to scoped_resources_builder_cm; dagstermill and all known custom implementations are updated accordingly
python_modules/dagster/dagster_tests/core_tests/test_job_init.py Good coverage: unit test for mapping logic, format tests for both new and fallback paths, and a monkeypatch regression test verifying job_def propagation depth
python_modules/libraries/dagstermill/dagstermill/manager.py Adds optional job_def param to _setup_resources and forwards it to resource_initialization_event_generator, keeping the drop-in contract intact

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant EG as execution_context_event_generator
    participant RIM as resource_initialization_manager
    participant RIEG as resource_initialization_event_generator
    participant CRIEG as _core_resource_initialization_event_generator
    participant GROM as get_resource_origins_mapping
    participant MSG as _resource_init_start_message

    EG->>RIM: "job_def=job_def (new)"
    RIM->>RIEG: "job_def=job_def (new)"
    RIEG->>CRIEG: "job_def=job_def (new)"
    CRIEG->>GROM: job_def (if not None)
    GROM-->>CRIEG: Mapping[resource_key to Set[op_name]]
    CRIEG->>MSG: resource_keys, resource_key_to_op_names
    MSG-->>CRIEG: enriched log string
    CRIEG-->>EG: RESOURCE_INIT_STARTED event with enriched message
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant EG as execution_context_event_generator
    participant RIM as resource_initialization_manager
    participant RIEG as resource_initialization_event_generator
    participant CRIEG as _core_resource_initialization_event_generator
    participant GROM as get_resource_origins_mapping
    participant MSG as _resource_init_start_message

    EG->>RIM: "job_def=job_def (new)"
    RIM->>RIEG: "job_def=job_def (new)"
    RIEG->>CRIEG: "job_def=job_def (new)"
    CRIEG->>GROM: job_def (if not None)
    GROM-->>CRIEG: Mapping[resource_key to Set[op_name]]
    CRIEG->>MSG: resource_keys, resource_key_to_op_names
    MSG-->>CRIEG: enriched log string
    CRIEG-->>EG: RESOURCE_INIT_STARTED event with enriched message
Loading

Reviews (1): Last reviewed commit: "Log which ops require each resource duri..." | Re-trigger Greptile

Comment on lines +417 to +418
if not resource_key_to_op_names:
return "Starting initialization of resources [{}].".format(", ".join(sorted(resource_keys)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The guard if not resource_key_to_op_names falls through to the old message format for both None (no job_def) and an empty dict {}. An empty dict is returned by get_resource_origins_mapping when every resource in the job is required only through the I/O layer or type system (e.g., a job whose ops have no explicit required_resource_keys). In that case job_def was provided and the enriched format should still be used — each resource would correctly display as "required by the I/O layer" — but the old flat-list fallback is emitted instead. The check should use an identity comparison with None.

Suggested change
if not resource_key_to_op_names:
return "Starting initialization of resources [{}].".format(", ".join(sorted(resource_keys)))
if resource_key_to_op_names is None:
return "Starting initialization of resources [{}].".format(", ".join(sorted(resource_keys)))

Comment on lines +146 to +150
resource_dependencies = resolve_resource_dependencies(job_def.resource_defs)
for resource_key, op_names in list(origins.items()):
for dep_key in get_dependencies(resource_key, resource_dependencies):
if dep_key != resource_key:
origins[dep_key].update(op_names)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 KeyError risk on unregistered resource keys

get_dependencies indexes into resource_dependencies with every key currently in origins. If an op (or hook) declares a required_resource_keys entry that is not present in job_def.resource_defs, resource_deps[resource_key] will raise a KeyError before the enriched log message is produced. In a valid, fully-validated job this can't happen, but if get_resource_origins_mapping is ever called before full job validation (e.g., during inspection or tooling), an uncaught KeyError would surface here as an obscure internal error rather than a clear validation message. A resource_key in resource_dependencies guard, or a resource_dependencies.get(resource_key, set()) call inside get_dependencies, would make the failure mode explicit.

@Mariomsoliman

Copy link
Copy Markdown
Author

Hey @sryza and @alangenfeld , opening this to fix #2307. It adds the missing context to the resource initialization log, so instead of just listing resource names, it now shows which ops actually need each one. Let me know if you'd want a different approach, happy to adjust. Thanks for taking a look!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Log the origin of required resources when initializing

1 participant