Skip to content

[rest] Do not replay a POST the server cannot absorb twice - #9122

Open
sundapeng wants to merge 3 commits into
apache:masterfrom
sundapeng:upstream/s2-do-not-replay-unsafe-post
Open

[rest] Do not replay a POST the server cannot absorb twice#9122
sundapeng wants to merge 3 commits into
apache:masterfrom
sundapeng:upstream/s2-do-not-replay-unsafe-post

Conversation

@sundapeng

Copy link
Copy Markdown
Member

Stacked on #9120 and the S1 PR — their commits show up here too until they merge. Review only
[rest] Do not replay a POST the server cannot absorb twice.

Purpose

The REST client retries a 429 or a 503 on any request, POST included. That is right for nearly
everything it sends: registering a partition, creating a database, committing a snapshot the server
already holds all land on the same state the second time, and the retry is the only defence against a
rate limiter or a restarting node.

It is wrong for a request that reports an increment. A 429 or a 503 can reach the client from an
intermediary after the server already applied the request
, so replaying it applies it again — and an
increment applied twice is a wrong number that no caller can see. Nothing in the response
distinguishes the two cases, which is why this has to be decided by the request rather than by the
status.

RESTRequest gains isRetrySafe(), defaulting to true so every existing request keeps the retry
it has today
. A request answering false is sent exactly once and the failure reaches the caller,
which knows whether re-sending is safe.

Two details a reviewer will ask about

  • The mark travels in the HttpClientContext rather than in the request, so it never reaches the wire
    and survives whatever the exec chain does to the request object.
  • isRetrySafe() is a getter on a serialized type, so it is @JsonIgnore; a test pins that it stays
    out of the body.

Compatibility

This PR adds no request that answers false. The first one arrives with the partition-statistics
work for catalog-managed format tables, and the ordering matters: with that in and this out, an
automatic retry double-counts an increment and the server cannot tell a redelivery from a second
genuine increment.

API and Format

RESTRequest.isRetrySafe() is a new default method returning true; existing implementations need
no change. No format change.

Documentation

The contract is in the method javadoc.

PartitionStatistics said only that its fields "may be negative, indicating that
some data has been removed". That covers one of the two planes the class is read
on, and consumers have been getting the other one wrong.

On the delta plane — what a commit changed — a negative value is a decrement the
server adds to what it holds. That is the existing meaning and nothing here
changes it. On the observation plane — what listPartitions returns for a
partition as it stands — a negative value means nobody ever reported that field,
and 0 means an exact zero. Conflating them is not cosmetic: a consumer that reads
unknown as zero plans against an empty partition that may hold a billion rows,
and one that does arithmetic on it gets a number that is wrong rather than
missing.

So the plane is named in the javadoc, unknown gets a name (UNKNOWN, with
isKnown() to test it rather than each caller comparing against -1), and unknown
is documented as per field: a reporter that only knows the file count leaves the
record count unknown and fills the rest.

The fields stay primitive. Boxing them to express unknown as null would be a
breaking change to a @public class, and the encoding above needs no new type.

FileSystemSplitEnumerator now says PartitionStatistics.UNKNOWN where it said -1.
Discovering partitions by listing directories measures nothing about what is
inside them, which is what unknown already meant there; this is the same value
under its own name.
…y counted

FormatTableRollingFileWriter counts every row it writes and
FormatTableSingleFileWriter knows the byte length of the file it closed. Both
numbers are then dropped: closeAndGetCommitters returns only the committers, and
prepareCommit wraps each one in a TwoPhaseCommitMessage that carries nothing
else. Anything downstream that wants to know what a commit wrote has to go back
to the filesystem and list it.

This keeps the two numbers attached to the file they describe, in a new
FormatTableWrittenFile that pairs the committer with them, and lets
TwoPhaseCommitMessage carry it. Nothing reads them yet.

TwoPhaseOutputStream.Committer is untouched. RenamingTwoPhaseOutputStream is
@public, so adding a method to the type its committer() returns would break
external implementations; the counts ride the paimon-core commit message instead.
The REST client retries a 429 or a 503 on any request, POST included. That is
right for nearly everything it sends: registering a partition, creating a
database, committing a snapshot the server already holds all land on the same
state the second time, and the retry is the only defence against a rate limiter
or a restarting node.

It is wrong for a request that reports an increment. A 429 or a 503 can reach the
client from an intermediary after the server already applied the request, so
replaying it applies it again — and an increment applied twice is a wrong number
that no caller can see. Nothing in the response distinguishes the two cases,
which is why this has to be decided by the request rather than by the status.

RESTRequest gains isRetrySafe(), defaulting to true so every existing request
keeps the retry it has today. A request answering false is sent exactly once and
the failure reaches the caller, which knows whether re-sending is safe. The mark
travels in the HttpClientContext rather than in the request, so it never reaches
the wire and survives whatever the exec chain does to the request object;
@JsonIgnore keeps the getter out of the serialized body as well, and a test pins
both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant