github-actions[bot] commented on code in PR #66262:
URL: https://github.com/apache/doris/pull/66262#discussion_r3683916791
##########
be/src/runtime/exec_env.h:
##########
@@ -318,11 +317,14 @@ class ExecEnv {
}
#ifdef BE_TEST
- void set_tmp_file_dir(std::unique_ptr<segment_v2::TmpFileDirs>
tmp_file_dirs) {
- this->_tmp_file_dirs = std::move(tmp_file_dirs);
- }
- void set_ready() { this->_s_ready = true; }
- void set_not_ready() { this->_s_ready = false; }
+ // Defined out of line on purpose: assigning the unique_ptr destroys the
old
+ // pointee, which would require TmpFileDirs to be COMPLETE in every
translation
+ // unit that includes this header -- and this header is included by most
of the
+ // backend. Keeping the body in the .cpp lets the forward declaration above
+ // suffice.
+ void set_tmp_file_dir(std::unique_ptr<segment_v2::TmpFileDirs>
tmp_file_dirs);
Review Comment:
[P2] Complete the direct `TmpFileDirs` include cleanup
Moving this setter out of line removes the definition that `exec_env.h` used
to
supply, but several direct consumers still compile only through unrelated
transitive chains. For example, `exec_env_init.cpp:194,239-240` destroys
`ExecEnv`, constructs `TmpFileDirs`, and calls `init()` without its owning
header;
the definition currently arrives through
`cloud_storage_engine.h -> txn_manager.h -> segment_writer.h ->
column_writer.h
-> ann_index_writer.h -> index_writer.h`. `compaction.cpp:1002`,
`segcompaction_test.cpp`, `schema_util_rowset_test.cpp`, and
`variant_column_writer_reader_test.cpp` have analogous chains. Removing any
of
those unrelated edges recreates the same incomplete-type build failure this
PR is
fixing. Please include `storage/index/index_writer.h` directly in these
consumers.
##########
build-support/check-header-deps.py:
##########
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Layering guard for BE headers.
+
+A handful of headers -- runtime/exec_env.h above all -- are included, directly
or
+not, by most of the backend. Anything they pull in becomes a dependency of
nearly
+every translation unit, so one stray include can multiply rebuild cost by an
order
+of magnitude and does so silently: the code still compiles, only the build
slows
+down. This script turns that into a build error instead.
+
+Each rule names a hub header and a directory prefix it must not reach. Keeping
a
+hub out of a subsystem is what lets that subsystem's headers be edited cheaply.
+
+Usage:
+ build-support/check-header-deps.py # enforce the rules
+ build-support/check-header-deps.py --report # rank headers by rebuild
cost
+"""
+
+import argparse
+import collections
+import os
+import re
+import sys
+
+INCLUDE = re.compile(r'^\s*#\s*include\s+"([^"]+)"')
+SOURCE_ROOTS = ("be/src", "be/test")
+INCLUDE_ROOT = "be/src"
+
+# (hub header, forbidden prefix, allowed exceptions, why).
+# A hub must not reach the forbidden subtree through ANY chain of includes,
except
+# through the listed headers. An exception is only appropriate for a header
that
+# carries declarations or plain data types and pulls in nothing of its own --
adding
+# one should be a deliberate decision, which is exactly why they are listed
here
+# instead of being inferred.
+RULES = [
+ (
+ "runtime/exec_env.h",
+ "storage/index/",
+ {
+ # A leaf statistics struct with no project includes of its own,
which
+ # storage/olap_common.h carries as a plain data type.
+ "storage/index/inverted/inverted_index_stats.h",
+ },
+ "ExecEnv only ever names index types as pointers and already
forward-declares "
+ "them; reaching the index implementation headers from here puts the
whole "
+ "index writer stack (and CLucene) in front of most of the backend",
+ ),
+]
+
+# Forward-declaration headers are the sanctioned way through a barrier: they
carry
+# declarations only, so they cost nothing to include.
+FWD_SUFFIX = "_fwd.h"
+
+
+def load_includes():
+ """Maps each repo file to the list of project headers it includes."""
+ includes = {}
+ for root in SOURCE_ROOTS:
+ for directory, _, names in os.walk(root):
+ for name in names:
+ if not name.endswith((".h", ".hpp", ".cpp", ".cc")):
+ continue
+ path = os.path.join(directory, name)
+ with open(path, encoding="utf-8", errors="ignore") as handle:
+ includes[path] = [
+ match.group(1)
+ for match in (INCLUDE.match(line) for line in handle)
+ if match
+ ]
+ return includes
+
+
+def resolve(header, includes):
+ """Maps an include spelling to a repo path, or None when it is external."""
+ path = os.path.join(INCLUDE_ROOT, header)
+ return path if path in includes else None
+
+
+def reachable(start, includes):
+ """Every header reachable from `start`, with the chain that got there."""
+ chains = {start: [start]}
+ frontier = [start]
+ while frontier:
+ current = frontier.pop()
+ path = resolve(current, includes)
+ if path is None:
+ continue
+ for nxt in includes[path]:
+ if nxt in chains:
+ continue
+ chains[nxt] = chains[current] + [nxt]
+ frontier.append(nxt)
+ return chains
+
+
+def translation_units_affected(includes):
+ """How many translation units each header can force a rebuild of."""
+ users = collections.defaultdict(set)
+ for path, headers in includes.items():
+ for header in headers:
+ users[header].add(path)
+ counts = {}
+ for header in users:
+ seen, frontier = set(), [header]
+ while frontier:
+ current = frontier.pop()
+ for user in users.get(current, ()):
+ if user in seen:
+ continue
+ seen.add(user)
+ if user.startswith(INCLUDE_ROOT + "/"):
Review Comment:
[P2] Traverse reverse edges through `be/test` headers
`load_includes()` records both `be/src` and `be/test`, but reverse traversal
re-enqueues only users under `INCLUDE_ROOT == "be/src"`. A current
`test.cpp -> test helper.h -> runtime header` chain therefore stops at the
helper.
A path-aware traversal reaches over 160 additional `.cpp` users for
`runtime/query_context.h` and more than 80 for `runtime/exec_env.h`, so
`--report`
materially understates the rebuild fanout it is meant to rank. Represent
users as
resolved paths and continue through headers under every configured source
root.
##########
build-support/check-header-deps.py:
##########
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Layering guard for BE headers.
+
+A handful of headers -- runtime/exec_env.h above all -- are included, directly
or
+not, by most of the backend. Anything they pull in becomes a dependency of
nearly
+every translation unit, so one stray include can multiply rebuild cost by an
order
+of magnitude and does so silently: the code still compiles, only the build
slows
+down. This script turns that into a build error instead.
+
+Each rule names a hub header and a directory prefix it must not reach. Keeping
a
+hub out of a subsystem is what lets that subsystem's headers be edited cheaply.
+
+Usage:
+ build-support/check-header-deps.py # enforce the rules
+ build-support/check-header-deps.py --report # rank headers by rebuild
cost
+"""
+
+import argparse
+import collections
+import os
+import re
+import sys
+
+INCLUDE = re.compile(r'^\s*#\s*include\s+"([^"]+)"')
+SOURCE_ROOTS = ("be/src", "be/test")
+INCLUDE_ROOT = "be/src"
+
+# (hub header, forbidden prefix, allowed exceptions, why).
+# A hub must not reach the forbidden subtree through ANY chain of includes,
except
+# through the listed headers. An exception is only appropriate for a header
that
+# carries declarations or plain data types and pulls in nothing of its own --
adding
+# one should be a deliberate decision, which is exactly why they are listed
here
+# instead of being inferred.
+RULES = [
+ (
+ "runtime/exec_env.h",
+ "storage/index/",
+ {
+ # A leaf statistics struct with no project includes of its own,
which
+ # storage/olap_common.h carries as a plain data type.
+ "storage/index/inverted/inverted_index_stats.h",
+ },
+ "ExecEnv only ever names index types as pointers and already
forward-declares "
+ "them; reaching the index implementation headers from here puts the
whole "
+ "index writer stack (and CLucene) in front of most of the backend",
+ ),
+]
+
+# Forward-declaration headers are the sanctioned way through a barrier: they
carry
+# declarations only, so they cost nothing to include.
+FWD_SUFFIX = "_fwd.h"
+
+
+def load_includes():
+ """Maps each repo file to the list of project headers it includes."""
+ includes = {}
+ for root in SOURCE_ROOTS:
+ for directory, _, names in os.walk(root):
+ for name in names:
+ if not name.endswith((".h", ".hpp", ".cpp", ".cc")):
+ continue
+ path = os.path.join(directory, name)
+ with open(path, encoding="utf-8", errors="ignore") as handle:
+ includes[path] = [
+ match.group(1)
+ for match in (INCLUDE.match(line) for line in handle)
+ if match
+ ]
+ return includes
+
+
+def resolve(header, includes):
+ """Maps an include spelling to a repo path, or None when it is external."""
+ path = os.path.join(INCLUDE_ROOT, header)
Review Comment:
[P2] Resolve quoted includes relative to their including file
`resolve()` only tries `be/src/<spelling>`, but C++ first resolves a quoted
include
beside the including file. This tree already has 43 such project edges (for
example `variant_column_reader.h` includes `"nested_group_provider.h"`). If a
chain from `exec_env.h` crosses one of these sibling edges before reaching
`storage/index/`, traversal stops and `enforce()` reports success; an
in-memory
`exec_env.h -> risk_bridge.h -> "local_dep.h" -> index_writer.h` case
reproduces
that false negative. Carry the current repository path through traversal and
resolve sibling paths before the configured include roots.
##########
build-support/check-header-deps.py:
##########
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Layering guard for BE headers.
+
+A handful of headers -- runtime/exec_env.h above all -- are included, directly
or
+not, by most of the backend. Anything they pull in becomes a dependency of
nearly
+every translation unit, so one stray include can multiply rebuild cost by an
order
+of magnitude and does so silently: the code still compiles, only the build
slows
+down. This script turns that into a build error instead.
+
+Each rule names a hub header and a directory prefix it must not reach. Keeping
a
+hub out of a subsystem is what lets that subsystem's headers be edited cheaply.
+
+Usage:
+ build-support/check-header-deps.py # enforce the rules
+ build-support/check-header-deps.py --report # rank headers by rebuild
cost
+"""
+
+import argparse
+import collections
+import os
+import re
+import sys
+
+INCLUDE = re.compile(r'^\s*#\s*include\s+"([^"]+)"')
+SOURCE_ROOTS = ("be/src", "be/test")
+INCLUDE_ROOT = "be/src"
+
+# (hub header, forbidden prefix, allowed exceptions, why).
+# A hub must not reach the forbidden subtree through ANY chain of includes,
except
+# through the listed headers. An exception is only appropriate for a header
that
+# carries declarations or plain data types and pulls in nothing of its own --
adding
+# one should be a deliberate decision, which is exactly why they are listed
here
+# instead of being inferred.
+RULES = [
+ (
+ "runtime/exec_env.h",
+ "storage/index/",
+ {
+ # A leaf statistics struct with no project includes of its own,
which
+ # storage/olap_common.h carries as a plain data type.
+ "storage/index/inverted/inverted_index_stats.h",
+ },
+ "ExecEnv only ever names index types as pointers and already
forward-declares "
+ "them; reaching the index implementation headers from here puts the
whole "
+ "index writer stack (and CLucene) in front of most of the backend",
+ ),
+]
+
+# Forward-declaration headers are the sanctioned way through a barrier: they
carry
+# declarations only, so they cost nothing to include.
+FWD_SUFFIX = "_fwd.h"
+
+
+def load_includes():
+ """Maps each repo file to the list of project headers it includes."""
+ includes = {}
+ for root in SOURCE_ROOTS:
+ for directory, _, names in os.walk(root):
+ for name in names:
+ if not name.endswith((".h", ".hpp", ".cpp", ".cc")):
+ continue
+ path = os.path.join(directory, name)
+ with open(path, encoding="utf-8", errors="ignore") as handle:
+ includes[path] = [
+ match.group(1)
+ for match in (INCLUDE.match(line) for line in handle)
+ if match
+ ]
+ return includes
+
+
+def resolve(header, includes):
+ """Maps an include spelling to a repo path, or None when it is external."""
+ path = os.path.join(INCLUDE_ROOT, header)
+ return path if path in includes else None
+
+
+def reachable(start, includes):
+ """Every header reachable from `start`, with the chain that got there."""
+ chains = {start: [start]}
+ frontier = [start]
+ while frontier:
+ current = frontier.pop()
+ path = resolve(current, includes)
+ if path is None:
+ continue
+ for nxt in includes[path]:
+ if nxt in chains:
+ continue
+ chains[nxt] = chains[current] + [nxt]
+ frontier.append(nxt)
+ return chains
+
+
+def translation_units_affected(includes):
+ """How many translation units each header can force a rebuild of."""
+ users = collections.defaultdict(set)
+ for path, headers in includes.items():
+ for header in headers:
+ users[header].add(path)
+ counts = {}
+ for header in users:
+ seen, frontier = set(), [header]
+ while frontier:
+ current = frontier.pop()
+ for user in users.get(current, ()):
+ if user in seen:
+ continue
+ seen.add(user)
+ if user.startswith(INCLUDE_ROOT + "/"):
+ frontier.append(user[len(INCLUDE_ROOT) + 1:])
+ counts[header] = sum(1 for f in seen if f.endswith((".cpp", ".cc")))
+ return counts
+
+
+def enforce(includes):
+ failures = 0
+ for hub, forbidden, allowed, why in RULES:
+ if resolve(hub, includes) is None:
+ print(f"error: rule names a missing header: {hub}",
file=sys.stderr)
+ failures += 1
+ continue
+ chains = reachable(hub, includes)
+ for header, chain in sorted(chains.items()):
+ if not header.startswith(forbidden) or header.endswith(FWD_SUFFIX):
+ continue
+ if header in allowed:
+ continue
+ failures += 1
+ print(f"error: {hub} must not reach {forbidden}*", file=sys.stderr)
+ print(f" reason: {why}", file=sys.stderr)
+ print(" chain: " + "\n -> ".join(chain), file=sys.stderr)
+ print(
+ " fix: forward-declare the type in the header and include
the "
+ "real header in the .cpp, or route it through a *_fwd.h",
+ file=sys.stderr,
+ )
+ break
+ return failures
+
+
+def report(includes):
+ counts = translation_units_affected(includes)
+ ranked = sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:30]
+ print(f"{'TUs rebuilt':>11} header")
+ for header, count in ranked:
+ print(f"{count:>11} {header}")
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--report",
+ action="store_true",
+ help="rank headers by how many translation units they force a rebuild
of",
+ )
+ args = parser.parse_args()
+
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ os.chdir(root)
+ includes = load_includes()
+
+ if args.report:
+ report(includes)
+ return 0
+
+ failures = enforce(includes)
Review Comment:
[P2] Wire this guard into an automated check
Nothing in the repository invokes this new script: `build.sh`, CMake,
`run-be-ut.sh`, the build-support runners, and the GitHub workflows contain
neither a direct call nor wildcard check discovery. Therefore a future
violation
still passes every build unless someone happens to run the script manually,
so
this does not yet turn the regression into the advertised build error.
Please add
it to a BE-gated CI/build step (and include this script itself in that
step's path
trigger) so its nonzero exit status is enforced.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]