ray-project/ray's RLlib and DAG layers carry the highest risk — 5 functions to fix first

Five critical-band functions across ray's RLlib, Ray Data, and autoscaler subsystems combine cyclomatic complexity as high as 116 with active commit churn, making them live regression risks at commit c24ae61.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk19.78Low
Hottest Functionfrom_config

Antipatterns Detected

complex_branching5exit_heavy5god_function5long_function5deeply_nested4cyclic_hub3hub_function2

Run this on your own codebase

Hotspots runs locally in under a minute — no account, no data leaves your machine.

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god function and why does it matter in ray?

A god function is a function that has accumulated so many distinct responsibilities that it effectively acts as a miniature program on its own — handling input validation, type dispatch, resource allocation, error recovery, and output construction all in one place. In ray, all five top hotspots are flagged as god functions, which means changes to any one of them risk inadvertently affecting multiple unrelated behaviors in the same edit. The practical consequence is that a god function is hard to test in isolation: any test must set up the full context for all responsibilities, not just the one being verified. In Python specifically, god functions with high fan-out are especially risky because the types flowing through them are resolved at runtime, so the coupling to all 28 or 55 downstream callees is not visible until execution.

How do I reduce cyclomatic complexity in Python?

The most effective first step is the extract-method refactoring: identify a coherent sub-decision within the function — for example, the space-inference fallback chain in `get_multi_agent_setup` or the platform-specific input polling in `confirm` — and move it into a private helper with a clear return type. A cyclomatic complexity above 10 warrants splitting; above 30 it should be treated as a blocking refactoring item before the next feature addition. For functions like `_get_or_compile` with a CC of 116, the goal should not be to reach CC 1 in one pass but to extract the largest coherent sub-problem — the BFS channel allocation — into its own method, which alone could cut the effective CC by a third or more. Decompose-conditional is the complementary technique: replace nested `if/elif` chains that test the same variable (like the type-dispatch branches in `from_config`) with a dispatch table or a strategy pattern, each arm of which becomes a separate, independently testable function.

Is ray actively maintained?

Yes, and the data is specific about it: all five top hotspots are in the 'fire' quadrant, meaning they combine high structural complexity with recent commit activity, and all five were touched within the last 3 days of the analysis at commit c24ae61. Each of the five functions has 1 touch in the last 30 days, and the broader repository has 7,221 functions in the fire quadrant with zero in the debt quadrant, meaning active development is co-located with structural complexity across the codebase. High activity and high structural complexity are not mutually exclusive — the data here is evidence of a large, actively developed distributed systems project where the complexity has grown alongside the feature set, not evidence of neglect.

How do I reproduce this analysis?

The Hotspots CLI is available at github.com/hotspots-dev/hotspots. To reproduce this exact result, check out ray-project/ray at commit c24ae61 with `git checkout c24ae61`, then run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command works on any local git repository without additional configuration — no config file is required to get started.

What does activity-weighted risk mean?

Activity-weighted risk combines a function's structural complexity — derived from its cyclomatic complexity, maximum nesting depth, and fan-out — with how frequently it has been changed in recent commits. A function with a cyclomatic complexity of 116 that has not been touched in two years scores meaningfully lower than one with a cyclomatic complexity of 30 that is touched every week, because the complex-but-dormant function has lower near-term regression probability. The prioritization is intentional: it surfaces functions where a developer is most likely to introduce a bug right now, not just where the code is abstractly complicated. For `_get_or_compile`, the combination of CC 116, fan-out 55, and a commit 3 days ago is what produces a critical-band fire-quadrant activity-weighted risk score of 19.43 — each factor amplifies the others.

At commit c24ae61, ray-project/ray contains 26,578 analyzed functions, of which 2,261 are in the critical band and 7,221 fall into the ‘fire’ quadrant — high complexity and high recent activity simultaneously. The top-ranked function, from_config in rllib/utils/from_config.py, carries an activity-weighted risk score of 19.78 and was touched 1 time in the last 30 days just 3 days ago, meaning any engineer editing it today is working inside a cyclomatic complexity of 75 with 28 distinct callees and no margin for misreading a branch. I would start there, and then move immediately to _get_or_compile in the compiled DAG layer, where a cyclomatic complexity of 116 makes it the single most structurally dangerous function in this scan.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
from_configrllib/utils/from_config.py19.875528
_get_or_compilepython/ray/dag/compiled_dag_node.py19.4116655
get_multi_agent_setuprllib/algorithms/algorithm_config.py19.4102721
convert_to_numpypython/ray/data/_internal/numpy_support.py19.327414
confirmpython/ray/autoscaler/_private/cli_logger.py19.252722

Large Repo Analysis

ray is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.

Repository-wide triage

Before narrowing to individual functions, it helps to see where the structural risk concentrates across the codebase.

Triage Band Distribution
Fire7221Watch19357

26,578 functions analyzed

Every function hotspots flagged lands in either ‘fire’ or ‘watch’ — there are no dormant-complexity debt functions and no fully inactive low-risk functions in this snapshot. That means the structural risk in ray is not hiding in untouched corners: it is co-located with active development across the board. The 7,221 fire-quadrant functions represent code that is both hard to reason about and currently moving.

Detected Antipatterns
Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Deeply Nested×4Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
Cyclic Hub×3Cyclic Hub
Participates in a call cycle with other high-traffic functions, creating circular dependency risk.
Hub Function×2Hub Function
Many other functions call this one — a change here ripples widely through callers.

All five top hotspots share the god_function, long_function, complex_branching, and exit_heavy patterns simultaneously. That cluster is a specific structural signature: functions that have absorbed too many responsibilities over time, express them through dense branching trees, and have grown too long to refactor in a single sitting. In Python, where duck typing means fan-out coupling is resolved at runtime rather than at the call site, the hub_function and cyclic_hub patterns on two and three of these functions respectively amplify the blast radius further — a caller passing the wrong duck-typed argument may not surface the failure until deep inside one of these control-flow trees.


from_config — from_config.py

from_config
rllib/utils/from_config.py
19.78
critical
CC 75
ND 5
FO 28
touches/30d 1

This function is RLlib’s universal object factory. The source excerpt makes its scope clear: it accepts a config that can be a dict, a string, a filename, a Python callable, a pre-instantiated object, or None — and it must resolve each of those cases into a constructed object using a class-level __type_registry__. That polymorphic dispatch responsibility is precisely what drives a cyclomatic complexity of 75. Each accepted input shape adds branches; each constructor-hint form adds more; error handling and deepcopy fallback paths add still more. The result is tagged with complex_branching, deeply_nested, exit_heavy, god_function, and long_function — every pattern in this scan — as well as cyclic_hub and hub_function, meaning it both calls 28 distinct functions and sits at the center of a call graph that routes through it repeatedly.

In Python, fan-out of 28 is more dangerous than it looks on paper. Because arguments are duck-typed, the branch that resolves type_ from a string key in __type_registry__ versus from a module path versus from a callable all exercise different subsets of those 28 callees, and a static reader cannot tell which path a given caller will actually take at runtime. The function was touched 3 days ago, making this a live regression risk, not backlog.

Cyclomatic Complexity 75
threshold: 10
Fan-Out 28
threshold: 15

Recommendation: Extract each constructor-hint resolution strategy into its own private method — one for dict configs, one for string/filename resolution, one for callable hints, one for pre-instantiated passthrough. The type_ resolution block visible in the excerpt is a natural extraction seam. This would reduce the CC by distributing branches across smaller functions and make each path independently testable. The cyclic_hub pattern suggests some callers may be calling from_config for only one or two of these paths — a dispatch table or strategy object would let those callers call narrower, cheaper functions directly.


_get_or_compile — compiled_dag_node.py

_get_or_compile
python/ray/dag/compiled_dag_node.py
19.43
critical
CC 116
ND 6
FO 55
touches/30d 1

This is the highest raw cyclomatic complexity in the entire scan at 116, and a fan-out of 55 makes it the broadest coupling point as well. From the docstring and excerpt, _get_or_compile handles DAG compilation: it allocates inter-actor communication channels in a breadth-first traversal of the DAG, wires up reader/writer relationships for each node, and sets up the submitter and output-fetcher handles that subsequent invocations will reuse. The function is designed to be idempotent — it caches results and returns early if already compiled — but the compilation path itself is a deeply nested BFS loop that branches on node type (ClassMethodNode, InputNode, MultiOutputNode), on whether a downstream node is a class method output, on whether a reader handle has already been seen, and on channel type.

A CC of 116 means there are at least 116 independent execution paths through this function. Each path is a required test case; most are probably not covered. The nesting depth of 6 means the deepest logic — the per-reader channel-type decisions visible in the excerpt — sits inside six levels of conditionals and loops, making it extremely difficult to reason about in isolation. Fan-out of 55 means a change here can touch over half of the distinct callees in the compiled DAG subsystem.

Cyclomatic Complexity 116
threshold: 30
Fan-Out 55
threshold: 15

The function was touched 3 days ago. Given that Compiled DAG is one of Ray’s newer high-performance execution paths, active iteration on a function this complex is a meaningful regression risk.

Recommendation: The BFS traversal and the per-node channel allocation are two distinct concerns sharing the same function body. I would extract the channel-allocation logic for ClassMethodNode into a _allocate_method_channels helper, and the reader-deduplication and MultiOutputNode handling into a _resolve_readers helper. This would lower the effective CC of _get_or_compile to roughly the BFS scaffolding, and let the channel-type branching be tested as a unit.


get_multi_agent_setup — algorithm_config.py

get_multi_agent_setup
rllib/algorithms/algorithm_config.py
19.42
critical
CC 102
ND 7
FO 21
touches/30d 1

This method compiles a fully-resolved multi-agent policy configuration from potentially incomplete inputs: it infers observation and action spaces from an env, a spaces dict, or stored config values; it resolves policy classes; it handles remote actor envs via ray.get; it handles gymnasium.vector.Env; it validates that agents mapping to the same policy ID don’t have conflicting spaces; and it constructs the is_policy_to_train callable. The docstring’s Raises section alone lists two distinct ValueError paths. A cyclomatic complexity of 102 and a maximum nesting depth of 7 reflect that responsibility honestly — each space-inference fallback adds a branch, and each agent-to-policy validation adds another nested check.

The nesting depth of 7 is the deepest in the top five, meaning some conditional logic sits seven levels deep inside the function. At that depth, the implicit state a reader must track to understand a single branch — which env type, which space source, which policy class fallback has been selected — spans most of the function’s local variable space. Exit-heavy is also flagged, meaning there are multiple early-return paths that short-circuit the setup under different conditions, each of which requires its own test to verify correctness.

Cyclomatic Complexity 102
threshold: 30
Max Nesting Depth 7
threshold: 4

Recommendation: The space-inference logic — trying env first, then the spaces dict, then stored config — is a well-bounded sub-problem that could be extracted into a _infer_spaces_for_policy method. Separating that from the policy-class resolution and the is_policy_to_train closure construction would make each sub-task testable in isolation and bring the CC of the outer method to a manageable range. The deeply_nested pattern here is particularly addressable with early-return guard clauses: flatten the outermost env-type branches by returning a partial result from a helper rather than nesting the rest of the function inside them.


convert_to_numpy — numpy_support.py

convert_to_numpy
python/ray/data/_internal/numpy_support.py
19.34
critical
CC 27
ND 4
FO 14
touches/30d 1

This function is Ray Data’s entry point for coercing arbitrary UDF column output into numpy arrays. The source excerpt shows a layered decision tree: if the input is already an ndarray, return immediately; if it’s a list of one array, use np.expand_dims as an optimization; if it’s a list of datetimes, delegate to a datetime converter; otherwise attempt a general conversion that handles array-like objects (e.g. torch.Tensor), inspects element shapes for heterogeneity, and falls back to a ragged ndarray representation for mixed-shape or byte-string data.

A CC of 27 is moderate rather than extreme, but the hub_function pattern is the more telling signal here: 14 distinct callees means this function touches a wide cross-section of the numpy and array-protocol surface, and in Python, each of those callees may behave differently depending on the runtime type of column_values. The exit_heavy pattern reflects the multiple early returns — at least three distinct return paths before the main conversion block, plus an exception re-raise. Each exit path is a coverage obligation, and the exception path in particular (Failed to convert column values) silently swallows the shape-inspection state before re-raising, which could make debugging conversion failures harder than it needs to be.

Cyclomatic Complexity 27
threshold: 10

This function was touched 3 days ago. Ray Data’s map_batches API is a high-traffic code path, so any regression in type-coercion behavior would have broad user-facing impact.

Recommendation: Extract the ragged-vs-uniform shape detection logic — the loop that populates shapes and has_object — into a _classify_column_elements helper that returns a typed result. This would reduce the nesting in the main conversion block, make the shape-inspection logic independently testable, and separate the classification concern from the numpy construction concern. Given the hub_function classification, I’d also verify that callers aren’t relying on the specific exception message format, since that coupling would make future changes to the error path more expensive.


confirm — cli_logger.py

confirm
python/ray/autoscaler/_private/cli_logger.py
19.19
critical
CC 52
ND 7
FO 22
touches/30d 1

At first glance, a confirmation dialog function with a CC of 52 and a nesting depth of 7 looks like a mismatch between name and complexity. The source excerpt explains why: confirm is handling at least four distinct execution environments — non-interactive mode (raises immediately), --yes auto-confirm, Unix stdin readline with optional timeout, and Windows (where select is unavailable and the function must busy-poll msvcrt.kbhit() with a manual timer loop). The Windows path alone, visible in the excerpt, adds several nested branches just to handle character-by-character input detection and timeout logic. Layered on top of this are the _abort, _default, and _timeout_s control parameters, each of which adds conditional paths.

A nesting depth of 7 in a CLI logger function is a strong signal that platform branching and timeout logic have accumulated inside a function whose callers expect a simple bool return. The god_function pattern confirms it has grown beyond its original scope. Fan-out of 22 means it reaches into formatting utilities, platform detection, stdin, and possibly terminal libraries, all of which are implicitly coupled through the function’s kwargs forwarding.

Cyclomatic Complexity 52
threshold: 10
Max Nesting Depth 7
threshold: 4

Recommendation: The Windows polling loop is the most tractable extraction target — it can become a _read_with_timeout_windows helper that returns a string, symmetric with the Unix readline path. Pulling both platform-specific input-reading strategies out of confirm would flatten the nesting from 7 to roughly 4, reduce the CC meaningfully, and make each platform path independently testable without needing to mock the entire confirmation flow. The _abort and _default path logic could then be handled at the top of confirm via guard clauses rather than nested conditionals.


Context functions worth watching

Several functions from Ray Data also appeared in the broader scan in the ‘watch’ quadrant — _create_pyarrow_compute_udf in expressions.py, and max, min, sum in pandas_block.py. All were touched 3 days ago and carry activity-weighted risk scores around 10–11. Their structural complexity is low (CC of 3–4), so they are not refactoring priorities, but their recent activity in the same timeframe as convert_to_numpy suggests the Ray Data layer is in an active development push. If convert_to_numpy is being changed, reviewing whether those watch-quadrant neighbors are also in scope for the same PR is worth the five minutes.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
exit_heavy5
god_function5
long_function5
deeply_nested4
cyclic_hub3
hub_function2

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

Reproduce This Analysis

git clone https://github.com/ray-project/ray
cd ray
git checkout c24ae61b20b9242ee091ca21fd69f26d8a8bd391
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Related Analyses