[client] Back off writes rejected by disk protection - #4293
Conversation
Merge apache/fluss main at 5d8c478 into Back-off-writes-rejected-by-disk-protection without rewriting the existing client backoff commit. Replace the obsolete remote start/end offset setters in RemoteLogFetcherTest with updateRemoteLogOffsets(0L, 30L, 30L), preserving the readable range and copied watermark used to clean up local segments. Validation: targeted Maven verify passed for RemoteLogFetcherTest, RemoteLogManifestOverlapTest and RemoteLogTabletOverlapTest (57 distinct test cases). Maven Checkstyle, Spotless, Apache RAT and diff checks passed. Co-authored-by: Codex <noreply@openai.com>
Resolve conflicts in fluss-client after per-partition bucket count changes: - RecordAccumulator: telescope constructors so both diskWriteBackoff and bucketAssignerFactory are carried; keep the disk-write backoff check inside the ready() drain loop (throttle handled by the merged inline check). - SenderTest: keep both new imports; unify table-info helpers to support the primaryKey and historicalPartitionEnabled toggles; use the 6-arg accumulator constructor with the test clock and custom backoff. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… signatures Resolve a semantic merge conflict: apache/main refactored RecordAccumulator.append to 3-arg and routeWritesTo to 4-arg (bucket assignment now happens internally via BucketAssignerFactory). Update the disk-protection tests to route bucket selection through TestingBucketAssigner instead of passing the bucket id positionally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
swuferhong
left a comment
There was a problem hiding this comment.
Hi, @fhan688 I suggest extracting an internal WriteThrottleController to unify the existing disk-write backoff and KV backpressure handling:
- Policies translate errors or pressure signals into a throttling reason, scope, and deadline.
- The controller maintains state separately for each reason, computes the effective deadline, handles cleanup, and wakes the Sender when the next eligible send time moves earlier.
- ready() queries the remaining delay, and drain() checks eligibility again before draining batches. Adding a new policy should not require changes to either path.
- If we later need QPS or bandwidth limits, we can introduce non-blocking permit acquisition before sending requests. If the goal is simply to suppress aggressive retries during write rejection, we could allow only a bounded number of probe requests after the backoff expires.
This would make disk-write backoff one policy within a reusable write-throttling mechanism and provide a natural extension point for future throttling scenarios.
I'd like to confirm that extracting an internal |
I'd prefer to do it in this PR: set up the architecture properly upfront and reduce the amount of refactoring needed later. WDYT @platinumhamburg |
Agree +1. |
Purpose
Linked issue: #4278
When disk protection rejects writes with
DISK_WRITE_LOCKED, the Java client can immediately retry and repeatedly resend the full payload. During sustained disk pressure, this generates excessive network traffic and consumes client CPU without making progress.This PR introduces bounded exponential backoff for Java Log and KV writers. Queued writes targeting a rejected bucket wait before being sent again, while other eligible buckets continue to make progress. Writes retry automatically when the backoff expires, subject to the existing retry budget.
This addresses the Java-client portion of the issue. Rust-client support is planned in a separate PR.
Brief change log
Add two client configuration options:
client.writer.disk-write-locked.backoff: initial backoff for writes rejected by disk protection; defaults to1s.client.writer.disk-write-locked.backoff-max: maximum backoff, including jitter; defaults to10s.The delay doubles with the existing batch retry count and uses ±20% jitter. Configuration must satisfy
1ms <= initial <= maximum <= 2147483647ms. Setting both options to the same value selects a fixed delay without jitter.Implement the following scheduling and retry behavior:
TableBucket. Historical writes sharing a physical destination share its backoff. Concurrent updates retain the longer remaining deadline.client.writer.batch-timeoutis zero.Tests
Validation used JDK 11.0.27 and Maven 3.8.6.
Passed checks
RecordAccumulatorTest(25 tests) andSenderTest(51 tests).DiskWriteBackoffITCase, covering real-cluster Log/KV rejection and recovery, successful reads after recovery, and the close timeout.git diff --check.Coverage includes exponential growth and capping, jitter, deadline boundaries, concurrent updates and cleanup, bucket isolation, KV backpressure interaction, both error delivery paths, historical destinations, idempotence, retry exhaustion, and Sender waiting and wakeup.
Local traffic comparison
The comparison used one writer, one bucket, one TabletServer, replication factor one, a 4096-byte string per single-row batch, zero batch timeout, and approximately five seconds of simulated disk protection. The patched client used the default backoff settings.
Recovery time measures the interval from lifting protection to the successful write callback. Payload counters exclude transport overhead. These are single local samples demonstrating reduced repeated sends; they do not establish production capacity or the absence of a normal-throughput regression.
Full-suite verification remains outstanding
verifyencountered a rolling-upgrade health assertion failure and metadata-update errors inFlussLakeTableITCaseandPartitionedTableITCase. It subsequently stalled duringFlussTableITCase.testAppendAndPolland was stopped after collecting a thread dump.ReplicaFetcherThreadTestcases were rejected by disk protection because actual test-host disk usage was approximately 97.3%, above the 85% threshold.commons-collections.FastHashMap. Maven runs therefore used-Dcheckstyle.skip=true, with source validation performed separately through the Checkstyle CLI as described above.API and Format
Adds two Java-client configuration options. Public method signatures, RPC messages, error codes, and storage formats are unchanged.
The implementation uses the existing
DISK_WRITE_LOCKEDerror code and requires upgrading the Java client or the connector that bundles it; no server protocol upgrade is required.Backoff applies per physical table bucket. Requests already in flight may still complete, and this mechanism does not impose a cluster-wide bandwidth limit. After protection is lifted, writes may wait for the remaining backoff window before retrying.
Documentation
Update
website/docs/maintenance/configuration.mdwith the two configuration options and their defaults, validation constraints, backoff behavior, interaction with KV backpressure, flush and close semantics, recovery delay, and client upgrade requirements.Generate the option descriptions through the existing configuration documentation generator and synchronize them into the maintenance configuration page.