Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract manifest generation from tasks #6565

Merged
merged 18 commits into from
Jan 24, 2023

Conversation

stu-k
Copy link
Contributor

@stu-k stu-k commented Jan 10, 2023

resolves #6357
resolves #6534

Description

Abstract manifest generation from directly inside the task classes to their instantiation.

Checklist

@cla-bot cla-bot bot added the cla:yes label Jan 10, 2023
@stu-k stu-k force-pushed the CT-1582/abstract-task-manifest-gen branch from ea5841a to a0a4d5b Compare January 10, 2023 18:28
@ChenyuLInx ChenyuLInx self-requested a review January 13, 2023 21:00
Copy link
Contributor

@ChenyuLInx ChenyuLInx left a comment

Choose a reason for hiding this comment

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

Overall looks good!

core/dbt/cli/main.py Outdated Show resolved Hide resolved
core/dbt/cli/main.py Outdated Show resolved Hide resolved
core/dbt/task/base.py Show resolved Hide resolved
core/dbt/task/compile.py Show resolved Hide resolved
core/dbt/task/generate.py Outdated Show resolved Hide resolved
core/dbt/task/runnable.py Outdated Show resolved Hide resolved
core/dbt/cli/main.py Outdated Show resolved Hide resolved
Comment on lines +146 to +150
# if there are no args, the decorator was used without params @decorator
# otherwise, the decorator was called with params @decorator(arg)
if len(args0) == 0:
return outer_wrapper
return outer_wrapper(args0[0])
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better more pythonic way to have a decorator accept args?

Copy link
Contributor

@ChenyuLInx ChenyuLInx left a comment

Choose a reason for hiding this comment

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

Overall looks good, I was able to confirm a run works as expected locally.

core/dbt/cli/requires.py Show resolved Hide resolved
core/dbt/task/base.py Show resolved Hide resolved
@stu-k stu-k marked this pull request as ready for review January 20, 2023 21:15
@stu-k stu-k requested a review from a team January 20, 2023 21:15
@stu-k stu-k requested review from a team as code owners January 20, 2023 21:15
@stu-k stu-k requested a review from gshank January 20, 2023 21:15
Copy link
Contributor

@jtcohen6 jtcohen6 left a comment

Choose a reason for hiding this comment

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

Great work on this @stu-k! I left some comments to contextualize a few of the interrelated pieces that this PR is touching.

I called out two required changes that I'd consider blocking to merge:

  • Now that we've converted more manifest-using commands, we'll need @requires.manifest on those commands as well
  • The build command/task needs different behavior for compile_manifest

Then, I think we should open up a separate ticket to take another look at abstracting manifest compilation / graph generation from tasks.

@@ -47,7 +42,7 @@ def _run_unsafe(self) -> agate.Table:

def run(self) -> RunOperationResultsArtifact:
start = datetime.utcnow()
self._runtime_initialize()
self.compile_manifest()
Copy link
Contributor

Choose a reason for hiding this comment

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

Current behavior: run-operation does not "compile" the manifest, and it does not support node selection, i.e. dbt run-operation --select ... --exclude .... (The former is a prerequisite for the latter.)

There is a compelling proposal for why run-operation should support node selection: #5005. (This might also be a step toward eventually supporting macros that ref() ephemeral models in run-operation, though this change by itself doesn't get us there. That's a long, separate story.)

The only downside is that compilation (= resolving ephemeral models + constructing the DAG) takes time, especially in large projects. There is no caching/reuse/diffing between invocations, in that way that there is for project parsing, so it's a fixed cost every time.

So: I don't think this is a bad change, I just want to call out that it is a change.

core/dbt/cli/requires.py Outdated Show resolved Hide resolved
core/dbt/task/compile.py Show resolved Hide resolved
def parse(ctx, **kwargs):
"""Parses the project and provides information on performance"""
click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {ctx.obj['flags']}")
# manifest generation and writing happens in @requires.manifeset
return None, True
Copy link
Contributor

Choose a reason for hiding this comment

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

The changes in this PR mean that:

  • dbt parse will always write the manifest (= overwrite manifest.json). IMO that's a good change! I'd opened an issue for it recently: [CT-1759] dbt parse should (over)write manifest.json by default #6534. So we can also remove --write-manifest as a parameter for this command, and in params.py.
  • We no longer have the option of dbt parse --compile, which allows us to output detailed performance timing that includes the DAG construction step (= resolving ephemeral model references + linking nodes/edges).

For the second point: I'd be happy with kicking that out of scope for this PR, and opening a tech debt ticket to track it. There's a bigger idea here: "Abstract manifest compilation / graph generation from tasks." Idea being, move compile_manifest() out from task initialization, into either a conditional step within @requires.manifest, or as its own @requires.graph step. Then, tasks would accept the graph as an argument.

Considerations there:

  • Manifest compilation / graph generation does require the adapter, and therefore RuntimeConfig, as an input
  • It mutates the manifest and returns a Graph
  • The build task produces + uses a different graph from all other tasks, with extra test edges
  • If we stored the graph on ctx.obj, and passed it into each task explicitly, we would unlock the ability for programmatic invocations to cache + reuse the graph between invocations. That could make a difference for performance in very large projects. (With the important caveat that the external application would be responsible for distinguishing between standard and build-specific graphs.)

Finally, a separate proposal, out of scope for this PR but highly relevant: The parse task should return the Manifest here, instead of None (#6547). That could be as simple as changing this last line to:

def parse(ctx, **kwargs) -> Tuple[Manifest, bool]:
    """Parses the project and provides information on performance"""
    # manifest generation and writing happens in @requires.manifest
    return ctx.obj["manifest"], True

And then:

from dbt.cli.main import dbtRunner
dbt = dbtRunner()
manifest, _ = dbt.invoke(['parse'])

There's a bit more refinement we should do on that proposal first, to make sure it's an idea we're happy with.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the context. I had spoken with Gerda about the function of the existing ParseTask class, and she had said I could take out the tracking. Adding it back in shouldn't be difficult, and could be done conditionally.

For your proposal of returning the manifest from the task directly, I think that's something we need to discuss on the exec team. I believe we should have a standard output for each of these functions, perhaps a dict with a result or message or whatever key should contain result, in case we want to add meta information to that result object later.

Copy link
Contributor

Choose a reason for hiding this comment

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

I had spoken with Gerda about the function of the existing ParseTask class, and she had said I could take out the tracking. Adding it back in shouldn't be difficult, and could be done conditionally.

This is fine with me too. Gerda consolidated all the events in this task, anyway, to just ParseCmdOut (in the main branch). We'll just want to remove that event now that it's no longer being called anywhere. For simplicity, let's track that in a separate issue, rather than try to do branch merging shenanigans here.


The point I was making above was around, dbt parse --compile would also conditionally include the step around manifest compilation / graph generation, which can be very slow (as we've seen in recent reports/issues from folks with large projects). Let's track that in the new issue, "Abstract manifest compilation / graph generation from tasks."


For your proposal of returning the manifest from the task directly, I think that's something we need to discuss on the exec team. I believe we should have a standard output for each of these functions, perhaps a dict with a result or message or whatever key should contain result, in case we want to add meta information to that result object later.

Agreed! Let's keep discussing in the separate linked issue. The biggest considerations are:

  • How to keep the results relatively consistent for all commands (although different tasks already return differently typed result objects)
  • Should we expose the full manifest, as part of our public API? Just a part of it? Should we call this "experimental" functionality (liable to change in future versions)? We have the power to decide these things!

Copy link
Contributor

Choose a reason for hiding this comment

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

We might want to call these changes out here:
https://docs.getdbt.com/guides/migration/versions/upgrading-to-v1.5

Did the proposed "Abstract manifest compilation / graph generation from tasks." issue ever get created?

In the meantime, I'm going to delete both of these from params.py as part of #6546 since they don't appear to do anything:

compile_parse = click.option(
"--compile/--no-compile",
envvar=None,
help="TODO: No help text currently available",
default=True,
)

write_manifest = click.option(
"--write-manifest/--no-write-manifest",
envvar=None,
help="TODO: No help text currently available",
default=True,
)

core/dbt/cli/main.py Outdated Show resolved Hide resolved
core/dbt/task/build.py Outdated Show resolved Hide resolved
core/dbt/cli/main.py Show resolved Hide resolved
core/dbt/cli/requires.py Show resolved Hide resolved
@stu-k stu-k requested a review from jtcohen6 January 23, 2023 18:18
Copy link
Contributor

@jtcohen6 jtcohen6 left a comment

Choose a reason for hiding this comment

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

Nice work @stu-k!

Could I ask you to open two follow-up issues that came up in conversation?

  1. Remove ParseCmdOut, now that the parse command isn’t doing any of its own custom event firing / logging. (Or, conditionally add the logging back.)
  2. Abstracting graph generation into a decorator, rather than a step buried within task initialization. We can plan to tackle it in the next few months, whether as part of "API-ification" (phase 2) or "library-ification" (support for programmatic invocations / dbt-server).

@stu-k
Copy link
Contributor Author

stu-k commented Jan 24, 2023

@jtcohen6 Created

@stu-k stu-k merged commit 92b7166 into feature/click-cli Jan 24, 2023
@stu-k stu-k deleted the CT-1582/abstract-task-manifest-gen branch January 24, 2023 17:05
def docs_generate(ctx, **kwargs):
"""Generate the documentation website for your project"""
config = RuntimeConfig.from_parts(ctx.obj["project"], ctx.obj["profile"], ctx.obj["flags"])
task = GenerateTask(ctx.obj["flags"], config)
task = GenerateTask(ctx.obj["flags"], ctx.obj["runtime_config"])
Copy link
Contributor

Choose a reason for hiding this comment

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

sorry that I'm catching this post-merge but - should GenerateTask be passed the manifest?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think so, it is inheriting CompileTask, the reason it need manifest is that generate task would go to the warehouse and fetch all info about the models in current dbt project.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch! docs generate also performs a full compile of all manifest nodes — unless --select is passed, in which case only those nodes; or if --no-compile is passed, in which case it doesn't

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like the issue is being addressed by @aranke in the cli test work here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants