Skip to content

[Analytics-Engine][MPP] Move MPP exchange placement into top-down CBO - #22833

Open
LantaoJin wants to merge 74 commits into
opensearch-project:mainfrom
LantaoJin:feature/mpp-top-down-traits
Open

[Analytics-Engine][MPP] Move MPP exchange placement into top-down CBO#22833
LantaoJin wants to merge 74 commits into
opensearch-project:mainfrom
LantaoJin:feature/mpp-top-down-traits

Conversation

@LantaoJin

@LantaoJin LantaoJin commented Aug 25, 2026

Copy link
Copy Markdown
Member

Description

This is a mentioned followup of #21844:
Exchange placement for MPP joins and distributed aggregates was decided by DistributionEnforcementPass
that ran after CBO , peeled the exchanges CBO had placed, and re-decided placement from scratch.
That existed because bottom-up Volcano optimises a subtree before its parent's requirement is known, so
distribution could not be a search dimension: every join was gathered to the coordinator on sight, and only a
later pass had the global view to fix it.

Calcite's top-down mode removes that constraint. This PR makes distribution a real trait so placement falls out
of search instead of being repaired afterwards.

What changed

Top-down trait propagation. setTopDownOpt(true) plus PhysicalNode hooks (passThroughTraits /
deriveTraits / getDeriveMode) on Project, Filter, Sort, Aggregate, Join and the exchanges, with
OpenSearchConvention.enforce supplying enforcers.

Cost model learns parallelism. Every legal join shape previously returned makeTinyCost(), so a
coordinator join looked as cheap as an N-way distributed one and top-down always picked the coordinator. Join
cost is now inputRows / parallelism, taken from the shuffle partition count or the broadcast probe estimate.

DistributionEnforcementPass deleted. Two single-purpose rewrites survive, run from PlannerImpl
alongside the existing OpenSearchTopKRewriter / OpenSearchSortPushdownRewriter.

Verification

check result
TPC-H sf=10, 3-worker cluster 20/22 (17/22 in #21844)
TPC-H sf=1, fresh cluster per query, 4 runs 22/22

SF=10 Benchmark (3-workers EC2 cluster, 32g heap)

q base feat P50 base (s) P50 feat (s) change feature shape
q1 PASS 4/4 PASS 4/4 0.3 0.3 ~same gather x1
q2 PASS 4/4 PASS 4/4 0.7 0.7 ~same shuf x12, gather x2, bcast x2 ⚠
q3 PASS 4/4 PASS 4/4 1.3 1.4 +8% (1.08× slower) shuf x2, gather x1, bcast x1
q4 PASS 4/4 PASS 4/4 0.6 0.6 ~same gather x2
q5 PASS 4/4 PASS 4/4 2.2 2.4 +9% (1.09× slower) shuf x10, gather x1
q6 PASS 4/4 PASS 4/4 0.1 0.1 ~same gather x1
q7 PASS 4/4 PASS 4/4 4.7 4.3 −9% (1.09× faster) shuf x8, gather x1, bcast x1 ⚠
q8 PASS 4/4 PASS 4/4 0.4 0.4 ~same shuf x12, gather x2, bcast x1 ⚠
q9 PASS 4/4 PASS 4/4 1.3 1.3 ~same shuf x8, gather x1, bcast x1
q10 PASS 4/4 PASS 4/4 0.8 0.8 ~same shuf x6, gather x1
q11 PASS 4/4 PASS 4/4 4.0 3.0 −25% (1.33× faster) shuf x4, gather x2, bcast x2 ⚠
q12 PASS 4/4 PASS 4/4 0.3 0.2 −33% (1.50× faster) gather x1, bcast x1
q13 PASS 4/4 PASS 4/4 1.0 1.1 +10% (1.10× slower) shuf x2, gather x1
q14 PASS 4/4 PASS 4/4 1.4 1.4 ~same shuf x2, gather x1
q15 FAIL 1/4 PASS 4/4 0.2 0.1 −50% (2.00× faster) gather x3
q16 PASS 4/4 PASS 4/4 1.3 2.0 +54% (1.54× slower) gather x4, shuf x2, bcast x1 ⚠
q17 FAIL 0/4 FAIL 0/4 n/a n/a
q18 FAIL 0/4 FAIL 0/4 n/a n/a
q19 FAIL 0/4 PASS 4/4 n/a 5.1 shuf x2, gather x1
q20 PASS 4/4 PASS 4/4 0.6 0.5 −17% (1.20× faster) gather x5, shuf x2, bcast x1 ⚠
q21 FAIL 0/4 PASS 4/4 n/a 7.0 shuf x20, gather x5, bcast x3
q22 PASS 4/4 PASS 4/4 0.3 0.3 ~same gather x3

PASS: baseline 17/22 -> feature 20/22. Zero PASS->FAIL.
Aggregate over the 18 queries timed on BOTH sides: 21.5s -> 20.9s = −3% (1.03× faster).
q19/q21 are excluded because the baseline has no time for them; including them would flatter the branch by
counting queries main cannot complete.

SF=100 Benchmark (8-workers EC2 cluster, 16g heap):

q base feat P50 base (s) P50 feat (s) feature shape feat heap
q1 PASS PASS 0.6 0.6 gather x1 34%
q2 PASS PASS 7.7 8.8 shuf x14, gather x2, bcast x1 46%
q3 PASS PASS 25.1 25.5 shuf x4, gather x1 65%
q4 FAIL PASS n/a 14.1 gather x2, shuf x2 ⚠ 65%
q5 PASS PASS 49.7 50.3 shuf x10, gather x1 63%
q6 PASS PASS 0.3 0.3 gather x1 65%
q7 FAIL PASS n/a 52.5 75%
q8 PASS FAIL 75.8 n/a 84%
q9 FAIL FAIL n/a n/a 84%
q10 FAIL FAIL n/a n/a 84%
q11 FAIL FAIL n/a n/a 85%
q12 FAIL PASS n/a 19.3 shuf x2, gather x1 86%
q13 FAIL PASS n/a 12.2 shuf x2, gather x1 77%
q14 FAIL PASS n/a 16.4 shuf x2, gather x1 78%
q15 FAIL PASS n/a 1.7 shuf x4, gather x3 ⚠ 73%
q16 FAIL FAIL n/a n/a 77%
q17 FAIL FAIL n/a n/a 94%
q18 FAIL FAIL n/a n/a 94%
q19 FAIL FAIL n/a n/a 94%
q20 FAIL FAIL n/a n/a 94%
q21 FAIL FAIL n/a n/a 94%
q22 FAIL FAIL n/a n/a 94%

PASS: baseline 6/22 -> feature 10/22. Five gains (q4, q7, q12, q13, q14) against one loss (q8).

Why q17 still failed in sf=10 with top-down CBO:

Both build an enormous grouping over the full 60M-row lineitem, and in both cases the grouping key is high-cardinality:

  • q17 — the correlated scalar subquery l_quantity < (select 0.2*avg(l_quantity) … where l_partkey = p_partkey) decorrelates to avg(l_quantity) group by l_partkey. The subquery's own predicate is only the correlation, so the Brand#23/MED BOX filter cannot be pushed into it: the aggregate reads all 60M rows and produces ~2M groups.
  • q18 — PASS when set analytics.mpp.shuffle.partitions=24. sum(l_quantity) group by l_orderkey over all 60M rows gives ~15M groups, and that semi-join subquery sits underneath a 3-way customer ⋈ orders ⋈ lineitem. lineitem is consumed twice in one query.

Related Issues

Resolves #[Issue number to be closed when this PR is merged]

Check List

  • Functionality includes testing.
  • API changes companion pull request created, if applicable.
  • Public documentation issue/PR created, if applicable.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

…e for top-down MPP planning

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…nd Filter

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…oin behind setTopDownOpt

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ator split rule

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…sm in the cost model

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…lelism-collapse charge

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ash partitioning

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ion provides

Signed-off-by: Lantao Jin <ltjin@amazon.com>
… models parallelism

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…orcement pass

Signed-off-by: Lantao Jin <ltjin@amazon.com>
… measured trait staleness

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…lized shuffles

Signed-off-by: Lantao Jin <ltjin@amazon.com>
… satisfies-gated enforcer

Signed-off-by: Lantao Jin <ltjin@amazon.com>
… trait staleness

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…and distributed-join inputs

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…o the enforcement pass

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ggregation toggle

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…annot move yet

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ize floor

Signed-off-by: Lantao Jin <ltjin@amazon.com>
… aggregates

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ecord the narrowing

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ed buffers

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…fuse co-partitioned join tiers

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…d child and null out unbuildable enforcers

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…re LIMIT

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…deleted derive rule

Signed-off-by: Lantao Jin <ltjin@amazon.com>
… where it is optimal

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…sidual failures were regressions

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ts branch

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…e deadlock"

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…fails in a minute"

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ts producers"

Signed-off-by: Lantao Jin <ltjin@amazon.com>
Signed-off-by: Lantao Jin <ltjin@amazon.com>
…ition"

Signed-off-by: Lantao Jin <ltjin@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 0e6070d: null

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

…n-traits

Signed-off-by: Lantao Jin <ltjin@amazon.com>
@LantaoJin
LantaoJin force-pushed the feature/mpp-top-down-traits branch from 0e6070d to d3c439b Compare August 31, 2026 06:27
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit d3c439b

@github-actions

Copy link
Copy Markdown
Contributor

✅ Gradle check result for d3c439b: SUCCESS

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 6c7ac24

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 6c7ac24: null

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

* needed (unlike {@link OpenSearchProject}), which makes this the simplest pass-through in the tree.
*/
@Override
public Pair<RelTraitSet, List<RelTraitSet>> passThroughTraits(RelTraitSet required) {

@expani expani Sep 2, 2026

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.

I don't see computeSelfCost hacks getting removed with this PR which removes the gains from having properly implemented Trait Propagation.

@LantaoJin LantaoJin Sep 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed as a criterion, with one correction on what's removable.

The method itself has to stay, the parallelism terms in it are new here and load-bearing. Before this PR every legal join shape returned makeTinyCost(), which is exactly why top-down always picked the coordinator: an N-way distributed join priced identically to a serial one. Traits alone don't fix that.

The makeInfiniteCost() legality branches are the hacks, and they survive for one reason: rules still construct nodes with hand-built trait sets, bypassing passThroughTraits/deriveTraits. The hooks are tight — Join accepts only SINGLETON on pass-through, derives only coord-singleton or key-matched WORKER+HASH, and Aggregate prohibits derivation for SINGLE/FINAL, so search can't derive an illegal shape. But the physical conversion rules build OpenSearchAggregate(SINGLE) and OpenSearchJoin at whatever traits the child has (including a partitioned scan, i.e. the under-count), and the split rules hand-build the broadcast shape that deriveTraits deliberately declines. Infinite cost is what currently prunes those.

So the fix is to stop hand-building traits in rules, have them convert() inputs to the distribution the operator requires and let deriveTraits produce the output, after which the Join shape check becomes an assertion rather than a cost. I'll take that here.

The one branch that can't go that way is AggregateSplitRule's PARTIAL: it builds at child.getTraitSet(), and at match time the child's distribution can still be Type.ANY, so the rule cannot check legality itself and cost has to prune it when the trait resolves. That's the missing "partitioned, specification irrelevant" value in OpenSearchDistribution (Type.ANY means unresolved, not anything acceptable), the same gap behind your other two comments, and adding it also deletes OpenSearchPartialAggregatePushdownRewriter.

Will attempt it again.

@expani expani left a comment

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.

Thanks for getting this started @LantaoJin If you are planning some quick bug fixes I would suggest keeping them separate from Trait Propagation

Have some high level comments on the general gating criteria for ensuring Trait Propagation is implemented properly

*
* @opensearch.internal
*/
public final class OpenSearchLargeJoinDistributionRewriter {

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.

Why do we need a rewrite for large Join distribution ?

We should be striving to remove rewrites as we go on.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because there are two named CBO gaps and an exit condition: isPureShardScanShape rejects any input subtree containing a Join (so an upper join never gets a WORKER+HASH alternative at all), and exchange cost has no memory/spill term (so gathering always prices cheaper). Measured worth: without it, sf=10 drops 18/22 → 14/22 with q3/q5/q7/q11/q21 hitting ReduceSizeExceededException.

*
* @opensearch.internal
*/
public final class OpenSearchPartialAggregatePushdownRewriter {

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.

There are multiple places that handle pushing down partial aggs. Another rewriter for doing it with Joins seems incorrect.

Also, it looks like we are introducing Join awareness in Aggregate split rules can we try to see how to keep it constrainted to individual relational operators instead ?

That should make the code simpler and remove a lot of these rules

@LantaoJin

LantaoJin commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Thanks for getting this started @LantaoJin If you are planning some quick bug fixes I would suggest keeping them separate from Trait Propagation

I did. see #22800 #22787 #22878 #22818 #22798 #22915 #22916 (I would appreciate it if you could review them as well). These are all bugfix PRs which are not related to MPP but found in developing this optimization and benchmarking. This PR now includes some of them since I need to make the TPC-H pass. With these PRs being merged, I will rebase here.

A PARTIAL aggregate rides its input's partitioning, so it cannot deliver a gather.
Passing a SINGLETON demand down to its input built PARTIAL-over-gathered — the shape
computeSelfCost then has to price at infinity — so the hook manufactured an alternative
only cost could reject. Decline it instead; the gather belongs above a PARTIAL (the
ExchangeReducer that FINAL reads), never below it.

Signed-off-by: Lantao Jin <ltjin@amazon.com>
DistributionAware was the bottom-up distribution contract the post-CBO
DistributionEnforcementPass consulted, as its own javadoc said. With that pass gone,
requiredInputDistribution has no production caller at all and deriveOutputDistribution has
one intra-class use in OpenSearchProject, so the interface left the tree carrying two
parallel trait APIs where only Calcite's PhysicalNode hooks are live.

Removes the interface and its eight implementations, keeps Project's output remapping as a
private helper, and rewrites the javadoc that still narrated the deleted pass as if it ran —
including the two TODO(trait-propagation) blocks that promised the migration this branch
performs. OpenSearchJoin's and OpenSearchAggregate's infinite-cost gates are documented as
what they actually are: the requirement declaration for a node that is both the HEP marking
output and the physical operator. Seeding those nodes UNRESOLVED to retire the gates was
measured and does not work — a demand for Type.ANY is satisfied by anything, so an ANY input
subset makes the parent's check skip and a per-partition SINGLE aggregate over a
hash-partitioned join wins on tiny cost.

DistributionAwareTests only exercised the deleted methods. JoinTraitDerivationTests replaces
it against deriveTraits/passThroughTraits and adds the first unit coverage of the RIGHT/FULL
invariant: those joins derive no hash alternative at all, because their null-extended rows
carry NULL left keys and no 'partitioned, specification unknown' value exists to describe the
output with.

Signed-off-by: Lantao Jin <ltjin@amazon.com>
OpenSearchAggregateSplitRule and OpenSearchPartialAggregatePushdownRewriter each built
FINAL(gather(PARTIAL(input))) with their own copy of the same assembly, the rewriter reaching
across for three of the rule's public statics. The only real difference is who owns the
exchange: convert() during CBO so Volcano can dedup it, buildReducer() post-CBO because the
satisfies-gated enforcer would insert nothing over an input whose trait set still carries
CBO's coordSingleton.

AggregatePartialFinalSplit now owns the assembly and takes that difference as a gather
function; the three helpers become private to it. Both call sites keep their own answer to
WHEN to split, which is the part that genuinely differs.

Also records why childForcesGather cannot become a trait check: a RelSubset can advertise a
partitioned trait while every operator inside it is only implementable via a gather, so
trusting the trait costs 7 CannotPlanException failures. Registering both alternatives instead
was measured too — no gain, 4 regressions.

Signed-off-by: Lantao Jin <ltjin@amazon.com>
@LantaoJin LantaoJin changed the title [Analytics Engine][MPP] Move MPP exchange placement into top-down CBO [Analytics-Engine][MPP] Move MPP exchange placement into top-down CBO Sep 4, 2026
@LantaoJin
LantaoJin force-pushed the feature/mpp-top-down-traits branch from 6c7ac24 to 8bbf7dd Compare September 9, 2026 07:54
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 8bbf7dd

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Gradle check result for 8bbf7dd: SUCCESS

… cost

Placement legality was a shape table per operator: OpenSearchJoin listed three legal
distributions with a per-input match check, OpenSearchAggregate priced SINGLE-over-partitioned
and PARTIAL-over-singleton at infinity. Each of those tables also had to SKIP an input whose
distribution was still Type.ANY, because ANY is Volcano's UNRESOLVED placeholder and judging an
unresolved trait as illegal prices out alternatives the memo has not finished expanding.

Every one of those skips was a hole. A node that cannot demand traits of its inputs (the HEP
marking output — convert() is a no-op in HEP) can only be seeded UNRESOLVED, and an unresolved
subset is then satisfied-by-anything: it reaches a parent, the parent's check skips, the
alternative wins on tiny cost, and the subset resolves to the cheapest concrete member. Measured
directly: a Union that skipped the check let a per-partition SINGLE aggregate through, and the
gather above concatenated three partial results without merging them.

hasUnresolvedInput() replaces the skips with one operator-independent invariant — an unresolved
input cannot be consumed, because nothing above it has a defined cost or a defined correctness —
applied in all eight operators that consume inputs. buildEnforcer declines it too: you cannot
gather data whose location is undecided, and allowing it produced the 'exchange on top of the
unresolved subtree' shape, which is always cheaper (it moves the operator's output instead of its
input) and would therefore always win.

Also passes a SINGLETON demand through verbatim in Join's co-located case rather than narrowing
it to COORDINATOR, so a locality-agnostic demand over two 1-shard inputs of the same table is
satisfied where the data already sits.

Signed-off-by: Lantao Jin <ltjin@amazon.com>
…infinite cost

A SINGLE aggregate is only correct over already-gathered input, and until now that requirement
lived in computeSelfCost as makeInfiniteCost() — legality expressed through the cost channel,
which is meant for ranking. It had to live there because the requirement was never stated as a
trait: passThroughTraits declined a SINGLETON demand for any splittable aggregate, so nothing ever
asked the aggregate to deliver a distribution, and the HEP marking rule stamped it with whatever
its child happened to carry.

The aggregate now DECLARES it. The marking rule seeds the node UNRESOLVED (it runs in HEP and
cannot demand anything of its input, so it must not claim a placement either), passThroughTraits
claims any SINGLETON demand and passes the required locality through verbatim, and the
SINGLE-over-partitioned infinite-cost branch is deleted: the illegal pair is no longer
constructible, so there is nothing left for cost to reject. The sibling invariant that keeps this
safe is hasUnresolvedInput() from the previous commit.

Sort and the window/pinned Project stop narrowing a SINGLETON demand to coordSingleton for the
same reason. The root asks for anySingleton (locality null), which a 1-shard SINGLETON(SHARD)
subtree already satisfies; narrowing it inserted a gather to move data that was already on one
node. What a window frame needs is that all rows are on ONE node, not that the node is the
coordinator.

The PARTIAL-over-singleton branch stays. OpenSearchAggregateSplitRule builds its PARTIAL at
child.getTraitSet(), which at match time can still be ANY, so that rule cannot check its own
legality and registers optimistically; retiring that branch needs a 'partitioned, specification
irrelevant' value in OpenSearchDistribution.

Signed-off-by: Lantao Jin <ltjin@amazon.com>
Eleven assertions moved. Grouped by why:

Fused worker tiers (CascadeShuffleProbeTests x4): co-partitioned join levels fuse into ONE worker
tier instead of lowering to two binary tiers, so the upper join consumes the lower join directly
and only the bottom join sits over two shuffles. assertFusedTier in the same file already asserted
this shape for the join-only cases; these four catch up for the agg-over-join cases.

Rule counts (RuleProfilingListenerTests x2): ExpandConversionRule drops to 0 — with the gather
placed by Convention.enforce there is no AbstractConverter left to expand. OpenSearchAggregateSplitRule
rises to 8 and 3 because an UNRESOLVED seed makes it match once per (aggregate, input-subset) pair;
that is search space, not extra plan nodes.

One-row estimates (AggregateSplitCostTests x2): with the estimate at the 1.0 floor, single-stage
over a gather is genuinely cheaper than two-phase aggregation, and CBO now picks it because the
shape is no longer forced by an infinite-cost branch. Renamed off 'partialStaysBelowExchange',
which contradicted the new shape. The invariant worth guarding still holds and is stated in the
javadoc: a PARTIAL above the gather remains unconstructible.

Fewer exchanges (DAGShapeTests x1): a single-shard self-join with a scalar aggregate collapses from
two stages to one — the co-located join delivers SINGLETON(SHARD) and nothing forces a gather.

Known single-shard regression (PlanShapeTests x2): an aggregate under a single-shard join or union
now sits over a gather it does not need, because the only concrete singleton alternative offered to
it is coordSingleton. Both tests carry the cause and the boundary in a comment. Four attempts to
remove it each moved the extra gather elsewhere instead. Multi-shard plans are unaffected: the
sf=10 TPC-H sweep scores 17/22 before and after, with identical exchange shapes in 21 of 22 queries
and P50 total 29.5s vs 29.7s.

Signed-off-by: Lantao Jin <ltjin@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 5fc726a

@github-actions

Copy link
Copy Markdown
Contributor

❌ Gradle check result for 5fc726a: QUEUE_TIMEOUT

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

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

Labels

skip-diff-analyzer Maintainer to skip code-diff-analyzer check, after reviewing issues in AI analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants