Skip to content

[Bug] Doris 4.1.0 leaks two Ranger PolicyRefresher threads after every metadata checkpoint #65524

Description

@RoggeDeior

Search before asking

  • I had searched in the issues and found no similar issues.

Version

DORIS_BUILD_VERSION=doris-4.1.0-rc03
DORIS_BUILD_HASH=5960d4cea0e6d2a08f62b45a07a662395d2bbcfc
Java=17.0.2
Deployment=Kubernetes
Mode=Cloud / compute-storage decoupled
Authorization=Apache Ranger
Ranger version=2.7.0
access_controller_type=ranger-doris

The build hash is the same commit used by the Doris 4.1.0 release tag.

What's Wrong?

I searched existing issues and found:

PR #45645 previously made RangerDorisAccessController a singleton. However, this fix is no longer effective in Doris 4.1.0 because the new RangerDorisAccessControllerFactory directly creates a new controller for every checkpoint Env.

Every successful metadata checkpoint creates two new Ranger PolicyRefresher threads. These threads are not stopped when the temporary checkpoint Env is destroyed.

The number of threads therefore increases continuously:

PolicyRefresher count
= 1 normal refresher created by main
+ checkpoint count × 2

In our test environment:

Journal-triggered checkpoints:       3
Cloud stale-image checkpoints:     135
Total checkpoints:                 138

PolicyRefresher created by main:      1
PolicyRefresher created by
leaderCheckpointer:                 276

Total PolicyRefresher threads:      277

The numbers match exactly:

1 + 138 × 2 = 277

Thread creator distribution from the FE logs:

Count  Thread
-----  ------------------
276    leaderCheckpointer
1      main

The creation rate also matches the default cloud checkpoint interval. With:

cloud_checkpoint_image_stale_threshold_seconds = 3600

one cloud checkpoint is triggered every hour, and two new PolicyRefresher threads are leaked per checkpoint, resulting in approximately 48 additional refresher threads per day.

Evidence from FE Logs

A checkpoint directly creates a new Ranger plugin and PolicyRefresher:

RuntimeLogger 2026-07-07 11:10:57,122 INFO
(leaderCheckpointer|143)
[Checkpoint.doCheckpoint():122]
Trigger checkpoint since last checkpoint journal id: 5117761
is less than current finalized journal id: 5162245

RuntimeLogger 2026-07-07 11:10:57,140 INFO
(leaderCheckpointer|143)
[Checkpoint.doCheckpoint():155]
begin to generate new image: image.5162245

RuntimeLogger 2026-07-07 11:10:57,151 INFO
(leaderCheckpointer|143)
[AccessControllerManager.loadAccessControllerPlugins():102]
Found Authentication Plugin Factories: ranger-doris from class path.

RuntimeLogger 2026-07-07 11:10:57,159 INFO
(leaderCheckpointer|143)
[RangerBasePlugin.init():308]
Created PolicyRefresher Thread(PolicyRefresher(serviceName=doris)-377)

Every checkpoint leaves two additional PolicyRefresher threads running permanently.

In our production environment, the FE accumulated:

1321 PolicyRefresher threads

At the default 30-second Ranger polling interval, this generated approximately:

1321 refreshers × 2 Ranger endpoints / 30 seconds
≈ 88 requests per second

The affected endpoints included:

/service/plugins/policies/download/doris
/service/roles/download/doris

This eventually caused Ranger Admin to accumulate millions of asynchronous plugin-info update tasks:

RangerTransactionService:
scheduled = 20,751,852
pending   = 5,334,642

Ranger Admin then experienced:

Old generation usage: approximately 96%
Full GC count: 5197
Full GC time: approximately 18 hours
java.lang.OutOfMemoryError: GC overhead limit exceeded

Tomcat Acceptor/Poller threads eventually disappeared, while port 6080 remained in LISTEN state with a full TCP accept backlog, making Ranger Admin appear running but inaccessible.

What You Expected?

Only one Ranger PolicyRefresher should exist per Doris FE process.

Creating or destroying a temporary checkpoint Env must not leave any long-running Ranger background threads.

How to Reproduce?

  1. Deploy Doris 4.1.0 in Cloud Mode.

  2. Configure Ranger authorization:

access_controller_type = ranger-doris
  1. Keep the default configuration:
cloud_checkpoint_image_stale_threshold_seconds = 3600
  1. Start FE and wait for several cloud metadata checkpoints.

  2. Count Ranger PolicyRefresher threads:

jcmd <FE_PID> Thread.print \
  | grep -c '^"PolicyRefresher(serviceName=doris)'
  1. Count checkpoint events:
grep -R -h \
  'Trigger checkpoint since' \
  /opt/apache-doris/fe/log | wc -l

grep -R -h \
  'Trigger checkpoint in cloud mode because latest image is expired' \
  /opt/apache-doris/fe/log | wc -l
  1. Count PolicyRefresher creation records:
grep -R -h \
  'Created PolicyRefresher Thread(PolicyRefresher(serviceName=doris)' \
  /opt/apache-doris/fe/log | wc -l

The PolicyRefresher count grows by two after every successful checkpoint.

Root Cause Analysis

The observed call chain is:

Checkpoint.doCheckpoint()
  -> Env.getCurrentEnv()
  -> EnvFactory.createEnv(true)
  -> new checkpoint Env
  -> new AccessControllerManager
  -> RangerDorisAccessControllerFactory.createAccessController()
  -> new RangerDorisAccessController("doris")
  -> RangerBasePlugin.init()
  -> new PolicyRefresher

Checkpoint.doCheckpoint() obtains and destroys a checkpoint Env twice during one successful checkpoint.

The relevant Env.getCurrentEnv() bytecode is equivalent to:

if (isCheckpointThread()) {
    if (CHECKPOINT == null) {
        CHECKPOINT = EnvFactory.getInstance().createEnv(true);
    }
    return CHECKPOINT;
}

However, Env.destroyCheckpoint() only clears the static reference:

public static void destroyCheckpoint() {
    if (CHECKPOINT != null) {
        CHECKPOINT = null;
    }
}

It does not close AccessControllerManager, clean up the Ranger plugin, or stop PolicyRefresher.

The Doris 4.1.0 factory directly creates a new controller:

https://github.com/apache/doris/blob/4.1.0/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/RangerDorisAccessControllerFactory.java

@Override
public RangerDorisAccessController createAccessController(
        Map<String, String> prop) {
    return new RangerDorisAccessController("doris");
}

RangerDorisAccessController in Doris 4.1.0 also no longer contains the singleton getInstance() implementation introduced by PR #45645.

Anything Else?

No response

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions