Skip to content

Add server ingestion OOM protection#18784

Merged
xiangfu0 merged 1 commit into
apache:masterfrom
xiangfu0:server-ingestion-oom-protection
Jun 20, 2026
Merged

Add server ingestion OOM protection#18784
xiangfu0 merged 1 commit into
apache:masterfrom
xiangfu0:server-ingestion-oom-protection

Conversation

@xiangfu0

@xiangfu0 xiangfu0 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds server ingestion OOM protection for realtime ingestion. When protection is enabled for a consuming
segment, the server checks JVM heap usage before fetching more realtime messages. If heap usage is at or above the
configured throttle threshold, the consuming loop waits locally and resumes after heap usage drops to the recovery
threshold.

The protection uses Pinot's existing JVM heap usage tracking (ResourceUsageUtils, backed by MemoryMXBean), which is
the same process-level heap source used by query OOM protection/accounting. It adds a separate realtime-ingestion
backpressure gate on top of that shared heap signal; it does not reuse per-query attribution or query kill logic.

The implementation keeps heap sampling, hysteresis, GC requests, and metrics in a single server-wide throttle state.
Each consuming thread only checks whether its table is enrolled and then reads the shared server throttle status, so the
per-table work stays light.

Default Behavior

With no config changes, Pinot servers do not hold or stop realtime ingestion for this feature.

Effective defaults:

Setting Default Effect
Server mode DISABLE Server-level protection is off unless explicitly changed.
Throttle threshold 0.95 When protection is active, ingestion waits when JVM heap usage is at or above 95%.
Recovery threshold 0.90 When protection is active, ingestion resumes when JVM heap usage drops to 90% or lower.
Check interval 1000 ms A throttled consuming loop rechecks the shared server throttle status once per second.
GC request interval 30000 ms While throttled, Pinot asks the JVM to run GC at most once every 30 seconds.

Default runtime behavior:

  • Does not stop, hold, or throttle ingestion unless the server mode is changed from DISABLE or a table explicitly sets
    oomProtection to ENABLE.
  • Uses server-local backpressure only; it does not pause the table through controller APIs or change stream offsets.
  • Applies only while a segment is in INITIAL_CONSUMING; catch-up paths such as CATCHING_UP and
    CONSUMING_TO_ONLINE are not gated.
  • Requests JVM GC while throttled, using the same GC-hint mechanism as query OOM protection, to avoid an
    ingestion-heavy server staying paused only because reclaimable objects are still counted as used heap.
  • Table-level oomProtection=ENABLE opts a table in even when the server mode is DISABLE or would otherwise
    skip the table.
  • Table-level oomProtection=DISABLE opts a table out even when the server mode would protect it.

Server Configuration

Server properties use the user-facing prefix pinot.server.instance.. They can be supplied in the server instance config
at startup, and they are also dynamic cluster configs: updating the same keys through the cluster config updates the
shared server throttle state at runtime without a server restart.

Cluster config values override the boot-time server instance config while present. Removing a cluster config key falls
back to the boot-time instance config value, or to the default if the instance config did not set it.

Enable Upsert And Dedup Protection

pinot.server.instance.ingestion.oom.protection.mode=UPSERT_DEDUP_ONLY
pinot.server.instance.ingestion.oom.protection.heapUsageThrottleThreshold=0.95
pinot.server.instance.ingestion.oom.protection.heapUsageRecoveryThreshold=0.90
pinot.server.instance.ingestion.oom.protection.checkIntervalMs=1000
pinot.server.instance.ingestion.oom.protection.gcIntervalMs=30000

Update Through Cluster Config

Use the same full pinot.server.instance.* names when updating cluster config:

curl -X POST "http://<controller-host>:<controller-port>/cluster/configs" \
  -H "Content-Type: application/json" \
  -d '{
    "pinot.server.instance.ingestion.oom.protection.mode": "UPSERT_DEDUP_ONLY",
    "pinot.server.instance.ingestion.oom.protection.heapUsageThrottleThreshold": "0.95",
    "pinot.server.instance.ingestion.oom.protection.heapUsageRecoveryThreshold": "0.90",
    "pinot.server.instance.ingestion.oom.protection.checkIntervalMs": "1000",
    "pinot.server.instance.ingestion.oom.protection.gcIntervalMs": "30000"
  }'

To disable the server-level policy dynamically:

curl -X POST "http://<controller-host>:<controller-port>/cluster/configs" \
  -H "Content-Type: application/json" \
  -d '{"pinot.server.instance.ingestion.oom.protection.mode": "DISABLE"}'

Server Config Reference

Property Default Description
pinot.server.instance.ingestion.oom.protection.mode DISABLE Dynamic server-level policy. Supported values: ENABLE, UPSERT_DEDUP_ONLY, DISABLE.
pinot.server.instance.ingestion.oom.protection.heapUsageThrottleThreshold 0.95 Dynamic server-wide heap usage ratio that starts ingestion backpressure.
pinot.server.instance.ingestion.oom.protection.heapUsageRecoveryThreshold 0.90 Dynamic server-wide heap usage ratio at or below which ingestion resumes. Must be lower than heapUsageThrottleThreshold.
pinot.server.instance.ingestion.oom.protection.checkIntervalMs 1000 Dynamic wait interval between shared throttle checks while ingestion is held.
pinot.server.instance.ingestion.oom.protection.gcIntervalMs 30000 Dynamic minimum interval between JVM GC requests while ingestion is held. Set to 0 or a negative value to disable the explicit GC request.

Server Modes

# Protect all realtime tables unless a table opts out.
pinot.server.instance.ingestion.oom.protection.mode=ENABLE

# Protect only upsert and dedup realtime tables unless a table opts in/out.
pinot.server.instance.ingestion.oom.protection.mode=UPSERT_DEDUP_ONLY

# Leave server-level protection disabled unless a table opts in.
pinot.server.instance.ingestion.oom.protection.mode=DISABLE

Tune Server-Wide Thresholds

Thresholds are server-level only. They are intentionally not table-level knobs.

pinot.server.instance.ingestion.oom.protection.heapUsageThrottleThreshold=0.95
pinot.server.instance.ingestion.oom.protection.heapUsageRecoveryThreshold=0.90
pinot.server.instance.ingestion.oom.protection.checkIntervalMs=2000
pinot.server.instance.ingestion.oom.protection.gcIntervalMs=30000

Table Configuration

Tables can only opt in or opt out with ingestionConfig.streamIngestionConfig.oomProtection. Thresholds are
configured on the server and shared by all enrolled realtime consuming threads.

oomProtection uses org.apache.pinot.spi.utils.Enablement:

  • DEFAULT: follow the server mode. This is also the behavior when the field is unset.
  • ENABLE: protect this realtime table even when the server mode is DISABLE or would otherwise skip it.
  • DISABLE: turn protection off for this realtime table even when the server mode would protect it.

Table Config Reference

Field Default Description
oomProtection DEFAULT Optional table override. Supported values: ENABLE, DISABLE, DEFAULT.

Force Protection On For One Realtime Table

Use this to opt in a specific realtime table, including when the server mode is DISABLE or UPSERT_DEDUP_ONLY would
skip a non-upsert/non-dedup table.

{
  "ingestionConfig": {
    "streamIngestionConfig": {
      "streamConfigMaps": [
        {
          "streamType": "kafka"
        }
      ],
      "oomProtection": "ENABLE"
    }
  }
}

Disable Protection For One Table

"oomProtection": "DISABLE"

Runtime Behavior

When protection is active for a consuming segment, the server waits before the next stream fetch. This reduces additional
heap pressure from realtime ingestion while leaving segment state, stream offsets, and table pause state unchanged.

The heap sample is the same process-level JVM heap usage used by query OOM protection. The ingestion feature does not
reuse per-query memory attribution or query kill logic; it only uses the shared heap usage signal to decide whether
realtime ingestion should wait.

Heap sampling, hysteresis, GC requests, and the active gauge are tracked centrally per server. A consuming thread does
not compute table-specific thresholds. It checks local enrollment (oomProtection plus server policy) and then
observes the shared server throttle state.

While throttled, Pinot requests JVM GC at a server-wide, rate-limited interval. The GC request timer resets when throttling releases, so a new throttle episode can request GC immediately before rate-limiting subsequent requests. This matters for ingestion-heavy servers:
once ingestion stops allocating, the JVM might not naturally trigger another collection immediately, so the reported used
heap can stay above the recovery threshold even when garbage is reclaimable. The explicit GC request is a JVM hint, just
like the query OOM pause path; JVM flags such as -XX:+DisableExplicitGC can still ignore it.

While waiting, the consume loop re-checks stop and segment end criteria at each interval, so force commit, time limit,
and stop paths do not wait indefinitely for heap recovery.

Metrics

This PR adds one server metric for ingestion-throttle visibility:

  • REALTIME_INGESTION_OOM_PROTECTION_ACTIVE: global gauge set to 1 when the server-wide realtime ingestion throttle
    is active, otherwise 0.

Heap usage is already reported by Pinot's existing OOM/memory reporting metrics, so this PR does not add a separate
realtime-ingestion heap percentage gauge.

Example Updated

The upsert meetup RSVP realtime example now includes a table opt-in sample and a short operator guide:

  • pinot-tools/src/main/resources/examples/stream/upsertMeetupRsvp/upsertMeetupRsvp_realtime_table_config.json
  • pinot-tools/src/main/resources/examples/stream/upsertMeetupRsvp/README.md

Testing

  • ./mvnw -pl pinot-core,pinot-common,pinot-segment-local -am -Dtest=ServerIngestionOomProtectionManagerTest,TableConfigSerDeUtilsTest,TableConfigUtilsTest -Dsurefire.failIfNoSpecifiedTests=false test
  • ./mvnw -pl pinot-core,pinot-server -am -Dtest=ServerIngestionOomProtectionManagerTest,RealtimeSegmentDataManagerTest -Dsurefire.failIfNoSpecifiedTests=false test
  • ./mvnw -pl pinot-segment-local,pinot-common,pinot-core -am -Dtest=TableConfigUtilsTest,TableConfigSerDeUtilsTest,RealtimeSegmentDataManagerTest -Dsurefire.failIfNoSpecifiedTests=false test
  • ./mvnw -pl pinot-plugins/pinot-metrics/pinot-yammer -am -Dtest=YammerServerPrometheusMetricsTest -Dsurefire.failIfNoSpecifiedTests=false test
  • ./mvnw spotless:apply -pl pinot-spi,pinot-common,pinot-core,pinot-segment-local,pinot-tools
  • ./mvnw license:format -pl pinot-spi,pinot-common,pinot-core,pinot-segment-local,pinot-tools
  • ./mvnw checkstyle:check -pl pinot-spi,pinot-common,pinot-core,pinot-segment-local,pinot-tools
  • ./mvnw license:check -pl pinot-spi,pinot-common,pinot-core,pinot-segment-local,pinot-tools
  • git diff --check

@xiangfu0 xiangfu0 force-pushed the server-ingestion-oom-protection branch from 6c03ba0 to 017ec1e Compare June 17, 2026 03:16
@xiangfu0 xiangfu0 requested a review from Copilot June 17, 2026 03:21

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

Adds server-local OOM protection/backpressure for realtime ingestion by sampling JVM heap usage and temporarily pausing stream fetches when usage exceeds configurable thresholds. This integrates into the server realtime consume loop, adds server + table override configs, and exposes new table-level metrics for observability.

Changes:

  • Introduces ServerIngestionOomProtectionManager and wires it into realtime consumption (gated in INITIAL_CONSUMING only).
  • Adds server instance config keys + table-level override config (serverIngestionOomProtectionConfig) with validation + SerDe/tests.
  • Adds new server metrics (gauges + meter) and updates the upsert realtime example docs/config.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pinot-tools/src/main/resources/examples/stream/upsertMeetupRsvp/upsertMeetupRsvp_realtime_table_config.json Adds table-level override example for server ingestion OOM protection.
pinot-tools/src/main/resources/examples/stream/upsertMeetupRsvp/README.md Documents server + table configs and behavior for the example.
pinot-spi/src/main/java/org/apache/pinot/spi/utils/CommonConstants.java Adds server instance config keys/defaults for OOM protection.
pinot-spi/src/main/java/org/apache/pinot/spi/config/table/ingestion/StreamIngestionConfig.java Adds table-level override field for OOM protection in stream ingestion config.
pinot-spi/src/main/java/org/apache/pinot/spi/config/table/ingestion/ServerIngestionOomProtectionConfig.java New table-level config object for OOM protection mode/threshold overrides.
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java Validates table-level OOM protection thresholds in ingestion config validation.
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java Adds validation tests for OOM protection config constraints.
pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/ServerIngestionOomProtectionManager.java New manager implementing heap-sampling + hysteresis + metrics/logging.
pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeTableDataManager.java Creates and resets the OOM protection manager per realtime table.
pinot-core/src/main/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManager.java Applies protection gating in consume loop for INITIAL_CONSUMING segments.
pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/ServerIngestionOomProtectionManagerTest.java Adds unit tests for policy/override behavior and wait/metrics paths.
pinot-core/src/test/java/org/apache/pinot/core/data/manager/realtime/RealtimeSegmentDataManagerTest.java Adds integration-style tests asserting gating is applied/skipped by state.
pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java Adds new table gauges for protection active + heap usage percent.
pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java Adds new table meter for throttling occurrences.
pinot-common/src/test/java/org/apache/pinot/common/utils/config/TableConfigSerDeUtilsTest.java Adds SerDe assertions for the new table-level config object.

@codecov-commenter

codecov-commenter commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.89163% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.74%. Comparing base (431d541) to head (871a1b2).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
.../realtime/ServerIngestionOomProtectionManager.java 77.10% 21 Missing and 17 partials ⚠️
...a/manager/realtime/RealtimeSegmentDataManager.java 50.00% 2 Missing and 5 partials ⚠️
.../pinot/server/starter/helix/BaseServerStarter.java 0.00% 3 Missing ⚠️
...server/starter/helix/HelixInstanceDataManager.java 0.00% 3 Missing ⚠️
...ata/manager/realtime/RealtimeTableDataManager.java 85.71% 1 Missing ⚠️
.../config/table/ingestion/StreamIngestionConfig.java 75.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #18784      +/-   ##
============================================
+ Coverage     57.00%   64.74%   +7.73%     
- Complexity        1     1319    +1318     
============================================
  Files          2599     3391     +792     
  Lines        151667   210891   +59224     
  Branches      24564    33105    +8541     
============================================
+ Hits          86459   136537   +50078     
- Misses        57884    63331    +5447     
- Partials       7324    11023    +3699     
Flag Coverage Δ
custom-integration1 100.00% <ø> (?)
integration 100.00% <ø> (?)
integration1 100.00% <ø> (?)
integration2 0.00% <ø> (?)
java-21 64.74% <73.89%> (+7.73%) ⬆️
temurin 64.74% <73.89%> (+7.73%) ⬆️
unittests 64.74% <73.89%> (+7.73%) ⬆️
unittests1 56.94% <72.08%> (-0.06%) ⬇️
unittests2 37.17% <32.01%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@xiangfu0 xiangfu0 added ingestion Related to data ingestion pipeline oom-protection Related to out-of-memory protection mechanisms labels Jun 17, 2026
@xiangfu0 xiangfu0 force-pushed the server-ingestion-oom-protection branch 4 times, most recently from 2264fdc to 1924588 Compare June 17, 2026 19:47
Comment thread pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java Outdated
Comment thread pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerMeter.java Outdated

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 15 out of 15 changed files in this pull request and generated 1 comment.

@xiangfu0 xiangfu0 force-pushed the server-ingestion-oom-protection branch 3 times, most recently from 95da13e to f32d50b Compare June 17, 2026 22:52
@xiangfu0 xiangfu0 force-pushed the server-ingestion-oom-protection branch from f32d50b to bd76b67 Compare June 18, 2026 07:51

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Found one high-signal issue; see inline comment.

@xiangfu0 xiangfu0 force-pushed the server-ingestion-oom-protection branch 6 times, most recently from f20a57f to 261dce0 Compare June 19, 2026 03:35
@xiangfu0 xiangfu0 force-pushed the server-ingestion-oom-protection branch from 261dce0 to a7f4623 Compare June 19, 2026 23:43
Comment thread pinot-common/src/main/java/org/apache/pinot/common/metrics/ServerGauge.java Outdated
@xiangfu0 xiangfu0 force-pushed the server-ingestion-oom-protection branch 4 times, most recently from 95141fb to a52d719 Compare June 20, 2026 01:16
@xiangfu0 xiangfu0 force-pushed the server-ingestion-oom-protection branch from a52d719 to 871a1b2 Compare June 20, 2026 02:45
@xiangfu0 xiangfu0 merged commit ff7df48 into apache:master Jun 20, 2026
11 checks passed
@xiangfu0 xiangfu0 deleted the server-ingestion-oom-protection branch June 20, 2026 04:15
@xiangfu0

Copy link
Copy Markdown
Contributor Author

Opened the corresponding docs update PR: pinot-contrib/pinot-docs#879

cypherean pushed a commit to cypherean/pinot that referenced this pull request Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ingestion Related to data ingestion pipeline oom-protection Related to out-of-memory protection mechanisms

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants