Repository: dbt-labs/dbt-core

Issue:
[CT-2317] `invocation_id` is cached between invocations in same Python process
Originally reported by @jeremyyeo:

> With dbt >= 1.4, we seem to be maintaining invocation_id across steps in a single run.

Starting in v1.4, `invocation_id` is now stored on `EventManager`:
https://github.com/dbt-labs/dbt-core/blob/65fcbe7b79c66b15a833fd9033117cc3f8ad3b3b/core/dbt/events/eventmgr.py#L185

I think we should clear or reset `invocation_id` as part of the steps in `cleanup_event_logger`:

https://github.com/dbt-labs/dbt-core/blob/0ef9931d197c996ccd4e3caf641729176eceda99/core/dbt/events/functions.py#L269-L272

https://github.com/dbt-labs/dbt-core/blob/0ef9931d197c996ccd4e3caf641729176eceda99/core/dbt/events/functions.py#L160-L165

### Reproduction case

- Create an `on-run-start` hook with `{{ log(invocation_id, info = true) }}`
- Create a dbt Cloud job with multiple run steps
- See if the invocation IDs are the same or different


Discussion:
We've confirmed that this is happening when multiple `dbt-core` commands are called from within the same Python process.

@peterallenwebb's recommendation:

> I do think calling `set_invocation_id()` between in-process dbt calls will solve the issue.

Also, to fix this issue going forward, and guarantee that programmatic invocations (`dbtRunner.invoke()`) have distinct values of `invocation_id`, we should either:
- Add a call to `set_invocation_id()` within `setup_event_logger()`
- Call `set_invocation_id()` within the `preflight` decorator, right before/after calling `setup_event_logger()`
- (Think about whether `EventManager` is the right place for `invocation_id()` to exist long-term)

Resolving patch:
diff --git a/core/dbt/cli/requires.py b/core/dbt/cli/requires.py
--- a/core/dbt/cli/requires.py
+++ b/core/dbt/cli/requires.py
@@ -9,7 +9,7 @@
 from dbt.cli.flags import Flags
 from dbt.config import RuntimeConfig
 from dbt.config.runtime import load_project, load_profile, UnsetProfile
-from dbt.events.functions import setup_event_logger, fire_event, LOG_VERSION
+from dbt.events.functions import fire_event, LOG_VERSION, set_invocation_id, setup_event_logger
 from dbt.events.types import (
     CommandCompleted,
     MainReportVersion,
@@ -41,15 +41,15 @@ def wrapper(*args, **kwargs):
         ctx.obj["flags"] = flags
         set_flags(flags)
 
+        # Logging
+        callbacks = ctx.obj.get("callbacks", [])
+        set_invocation_id()
+        setup_event_logger(flags=flags, callbacks=callbacks)
+
         # Tracking
         initialize_from_flags(flags.SEND_ANONYMOUS_USAGE_STATS, flags.PROFILES_DIR)
         ctx.with_resource(track_run(run_command=flags.WHICH))
 
-        # Logging
-        # N.B. Legacy logger is not supported
-        callabcks = ctx.obj.get("callbacks", [])
-        setup_event_logger(flags=flags, callbacks=callabcks)
-
         # Now that we have our logger, fire away!
         fire_event(MainReportVersion(version=str(installed_version), log_version=LOG_VERSION))
         flags_dict_str = cast_dict_to_dict_of_strings(get_flag_dict())
diff --git a/core/dbt/events/eventmgr.py b/core/dbt/events/eventmgr.py
--- a/core/dbt/events/eventmgr.py
+++ b/core/dbt/events/eventmgr.py
@@ -213,10 +213,6 @@ def add_logger(self, config: LoggerConfig):
         logger.event_manager = self
         self.loggers.append(logger)
 
-    def add_callbacks(self, callbacks: List[Callable[[EventMsg], None]]):
-        """Helper for adding a sequence of Callbacks to the EventManager"""
-        self.callbacks.extend(callbacks)
-
     def flush(self):
         for logger in self.loggers:
             logger.flush()
diff --git a/core/dbt/events/functions.py b/core/dbt/events/functions.py
--- a/core/dbt/events/functions.py
+++ b/core/dbt/events/functions.py
@@ -28,7 +28,7 @@
 def setup_event_logger(flags, callbacks: List[Callable[[EventMsg], None]] = []) -> None:
     cleanup_event_logger()
     make_log_dir_if_missing(flags.LOG_PATH)
-    EVENT_MANAGER.add_callbacks(callbacks=callbacks)
+    EVENT_MANAGER.callbacks = callbacks.copy()
 
     if ENABLE_LEGACY_LOGGER:
         EVENT_MANAGER.add_logger(


Test patch:
diff --git a/tests/adapter/dbt/tests/adapter/basic/test_docs_generate.py b/tests/adapter/dbt/tests/adapter/basic/test_docs_generate.py
--- a/tests/adapter/dbt/tests/adapter/basic/test_docs_generate.py
+++ b/tests/adapter/dbt/tests/adapter/basic/test_docs_generate.py
@@ -341,7 +341,6 @@ def verify_metadata(metadata, dbt_schema_version, start_time):
     assert metadata["dbt_version"] == dbt.version.__version__
     assert "dbt_schema_version" in metadata
     assert metadata["dbt_schema_version"] == dbt_schema_version
-    assert metadata["invocation_id"] == dbt.tracking.active_user.invocation_id
     key = "env_key"
     if os.name == "nt":
         key = key.upper()
diff --git a/tests/functional/artifacts/test_artifacts.py b/tests/functional/artifacts/test_artifacts.py
--- a/tests/functional/artifacts/test_artifacts.py
+++ b/tests/functional/artifacts/test_artifacts.py
@@ -334,7 +334,6 @@ def verify_metadata(metadata, dbt_schema_version, start_time):
     assert metadata["dbt_version"] == dbt.version.__version__
     assert "dbt_schema_version" in metadata
     assert metadata["dbt_schema_version"] == dbt_schema_version
-    assert metadata["invocation_id"] == dbt.tracking.active_user.invocation_id
     key = "env_key"
     if os.name == "nt":
         key = key.upper()
diff --git a/tests/functional/sources/test_source_fresher_state.py b/tests/functional/sources/test_source_fresher_state.py
--- a/tests/functional/sources/test_source_fresher_state.py
+++ b/tests/functional/sources/test_source_fresher_state.py
@@ -90,7 +90,6 @@ def _assert_freshness_results(self, path, state):
             == "https://schemas.getdbt.com/dbt/sources/v3.json"
         )
         assert data["metadata"]["dbt_version"] == dbt.version.__version__
-        assert data["metadata"]["invocation_id"] == dbt.tracking.active_user.invocation_id
         key = "key"
         if os.name == "nt":
             key = key.upper()
diff --git a/tests/functional/sources/test_source_freshness.py b/tests/functional/sources/test_source_freshness.py
--- a/tests/functional/sources/test_source_freshness.py
+++ b/tests/functional/sources/test_source_freshness.py
@@ -80,7 +80,6 @@ def _assert_freshness_results(self, path, state):
             == "https://schemas.getdbt.com/dbt/sources/v3.json"
         )
         assert data["metadata"]["dbt_version"] == dbt.version.__version__
-        assert data["metadata"]["invocation_id"] == dbt.tracking.active_user.invocation_id
         key = "key"
         if os.name == "nt":
             key = key.upper()
diff --git a/tests/unit/test_invocation_id.py b/tests/unit/test_invocation_id.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/test_invocation_id.py
@@ -0,0 +1,24 @@
+from dbt.cli.main import dbtRunner
+
+
+class TestInvocationId:
+    def test_invocation_id(self):
+        """Runs dbt programmatically twice, checking that invocation_id is
+        consistent within an invocation, but changes for the second invocation."""
+
+        runner = dbtRunner()
+
+        # Run once...
+        first_ids = set()
+        runner.callbacks = [lambda e: first_ids.add(e.info.invocation_id)]
+        runner.invoke(["debug"])
+
+        # ...run twice...
+        second_ids = set()
+        runner.callbacks = [lambda e: second_ids.add(e.info.invocation_id)]
+        runner.invoke(["debug"])
+
+        # ...check that the results were nice.
+        assert len(first_ids) == 1  # There was one consistent invocation_id for first run...
+        assert len(second_ids) == 1  # ...as well as the second...
+        assert len(first_ids.intersection(second_ids)) == 0