Skip to content

HIVE-29818: ServletSecurity needs a UGI cache - #6704

Open
henrib wants to merge 2 commits into
apache:masterfrom
henrib:HIVE-29818
Open

HIVE-29818: ServletSecurity needs a UGI cache#6704
henrib wants to merge 2 commits into
apache:masterfrom
henrib:HIVE-29818

Conversation

@henrib

@henrib henrib commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

The REST Catalog creates a fresh proxy UserGroupInformation per request via UserGroupInformation.createProxyUser. Hadoop's FileSystem.CACHE retains a reference to every such UGI (and its RPC/IPC resources), so under proxy authentication these short-lived UGIs accumulate and eventually exhaust memory in long-running deployments.

This PR caches the proxy UGI in ServletSecurity using a bounded, idle-evicting Caffeine cache keyed by (realUser, loginUser). Evicted entries release their resources via FileSystem.closeAllForUGI. Two new config vars tune the cache:

  • metastore.catalog.servlet.ugi.cache.size (default 1000)
  • metastore.catalog.servlet.ugi.cache.expiry (default 3600s, 0 disables expiry)

A note in the code documents why eviction-while-in-use is not reference-counted: eviction is idle-based (expireAfterAccess) and both the expiry window and max size are expected to be kept well above the longest operation / peak concurrent distinct users.

Why are the changes needed?

To prevent the OutOfMemoryError caused by unbounded accumulation of proxy UGIs and their associated FileSystem/IPC resources in long-running REST Catalog deployments.

Does this PR introduce any user-facing change?

Two new (optional) metastore configuration properties, both with sensible defaults.

How was this patch tested?

Added TestServletSecurity covering per-user caching, distinct proxies per user, eviction-triggered FileSystem.closeAllForUGI cleanup, and disabled expiry. All 4 tests pass.

The REST Catalog creates a fresh proxy UserGroupInformation per request via
UserGroupInformation.createProxyUser. Hadoop's FileSystem.CACHE retains a
reference to every such UGI (and its RPC/IPC resources), so under proxy
authentication these short-lived UGIs accumulate and eventually exhaust memory
in long-running deployments.

Cache the proxy UGI in ServletSecurity with a bounded, idle-evicting Caffeine
cache keyed by (realUser, loginUser). Evicted entries release their resources
via FileSystem.closeAllForUGI. Two new config vars tune the cache:
  - metastore.catalog.servlet.ugi.cache.size   (default 1000)
  - metastore.catalog.servlet.ugi.cache.expiry (default 3600s, 0 disables)

Add TestServletSecurity covering per-user caching, distinct proxies per user,
eviction-triggered FileSystem cleanup, and disabled expiry.
Copilot AI lite review requested due to automatic review settings August 17, 2026 15:38

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (4)

standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:123

  • The UgiKey field name realUser is misleading given the Javadoc: this value represents the effective/proxy user being impersonated, while the real user is the login user. Renaming the record components to something like effectiveUser and loginUser (or loginUserName) would reduce confusion and make logs like key.realUser() accurate.
  /**
   * Cache key for a proxy {@link UserGroupInformation}. A proxy UGI is bound to both the effective user it
   * impersonates and the server login user acting as its real user, so both participate in identity.
   */
  record UgiKey(String realUser, String loginUser) {}

standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:137

  • Casting the configured cache size from long to int can truncate large configured values (or wrap negative), leading to an incorrect maximumSize and unexpected behavior. Consider keeping this as a long end-to-end (Caffeine’s maximumSize accepts a long) and validating the value (e.g., reject negatives).
    this.proxyUserCache = createCacheWithConfig(
        MetastoreConf.getTimeVar(conf, MetastoreConf.ConfVars.CATALOG_SERVLET_UGI_CACHE_EXPIRY, TimeUnit.MILLISECONDS),
        (int) MetastoreConf.getLongVar(conf, MetastoreConf.ConfVars.CATALOG_SERVLET_UGI_CACHE_SIZE));

standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:170

  • Using .executor(Runnable::run) makes the removal listener run inline on the calling thread (likely a request thread). Since the listener calls FileSystem.closeAllForUGI, eviction can add noticeable latency or block request handling during bursts/evictions. Consider using the default executor or a dedicated bounded executor for removals so cleanup work doesn’t run on latency-sensitive threads.
    Caffeine<UgiKey, UserGroupInformation> builder = Caffeine.<UgiKey, UserGroupInformation>newBuilder()
        .maximumSize(maxSize)
        .executor(Runnable::run)
        .removalListener(cleanupListener);

standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:196

  • Logging the effective username at INFO can leak user identity information into logs and may be considered sensitive in some deployments. Since this log happens for cache misses (and potentially many distinct users), consider downgrading it to DEBUG or making it configurable/redacted.
  UserGroupInformation getProxyUser(String userName, UserGroupInformation loginUser) {
    return proxyUserCache.get(new UgiKey(userName, loginUser.getUserName()), key -> {
      LOG.info("Creating proxy user for: {}", key.realUser());
      return UserGroupInformation.createProxyUser(key.realUser(), loginUser);
    });
  }

@ayushtkn ayushtkn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanx @henrib this overall looks good to me, there are some suppressed comments from co-pilot but I feel they are minor and maybe worth addressing, can u give a check once

standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:123

    The UgiKey field name realUser is misleading given the Javadoc: this value represents the effective/proxy user being impersonated, while the real user is the login user. Renaming the record components to something like effectiveUser and loginUser (or loginUserName) would reduce confusion and make logs like key.realUser() accurate.

  /**
   * Cache key for a proxy {@link UserGroupInformation}. A proxy UGI is bound to both the effective user it
   * impersonates and the server login user acting as its real user, so both participate in identity.
   */
  record UgiKey(String realUser, String loginUser) {}

standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:137

    Casting the configured cache size from long to int can truncate large configured values (or wrap negative), leading to an incorrect maximumSize and unexpected behavior. Consider keeping this as a long end-to-end (Caffeine’s maximumSize accepts a long) and validating the value (e.g., reject negatives).

    this.proxyUserCache = createCacheWithConfig(
        MetastoreConf.getTimeVar(conf, MetastoreConf.ConfVars.CATALOG_SERVLET_UGI_CACHE_EXPIRY, TimeUnit.MILLISECONDS),
        (int) MetastoreConf.getLongVar(conf, MetastoreConf.ConfVars.CATALOG_SERVLET_UGI_CACHE_SIZE));

standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:170

    Using .executor(Runnable::run) makes the removal listener run inline on the calling thread (likely a request thread). Since the listener calls FileSystem.closeAllForUGI, eviction can add noticeable latency or block request handling during bursts/evictions. Consider using the default executor or a dedicated bounded executor for removals so cleanup work doesn’t run on latency-sensitive threads.

    Caffeine<UgiKey, UserGroupInformation> builder = Caffeine.<UgiKey, UserGroupInformation>newBuilder()
        .maximumSize(maxSize)
        .executor(Runnable::run)
        .removalListener(cleanupListener);

standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/ServletSecurity.java:196

    Logging the effective username at INFO can leak user identity information into logs and may be considered sensitive in some deployments. Since this log happens for cache misses (and potentially many distinct users), consider downgrading it to DEBUG or making it configurable/redacted.

  UserGroupInformation getProxyUser(String userName, UserGroupInformation loginUser) {
    return proxyUserCache.get(new UgiKey(userName, loginUser.getUserName()), key -> {
      LOG.info("Creating proxy user for: {}", key.realUser());
      return UserGroupInformation.createProxyUser(key.realUser(), loginUser);
    });
  }

- Rename UgiKey.realUser to effectiveUser (impersonated user, not the Hadoop real user)
- Keep cache size as long end-to-end, dropping the truncating int cast
- Run removal-listener cleanup on ForkJoinPool.commonPool() instead of the request thread;
  add a test-only constructor to inject a synchronous executor
- Downgrade proxy-user creation log from INFO to DEBUG
@sonarqubecloud

Copy link
Copy Markdown

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants